Icu 17092 replace hosts connect modal with table in target detail page (#2841)

* feat: 🎸 add host source table to target detail view

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17092

* chore: 🤖 remove unused translation

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17092

* test: 💍 update e2e tests for host source change

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17092

* test: 💍 add test for target details main connect button

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17092

* test: 💍 update e2e test selector

✅ Closes: https://hashicorp.atlassian.net/browse/ICU-17092
This commit is contained in:
Cameron Perera authored and GitHub committed 2025-05-23 11:45:27 -05:00
1 parent a747614a74
commit 96d7c055b0
8 files changed
+120 -316

No files matched your search

+10 -6
View File
@@ -166,13 +166,17 @@ test.describe('Targets tests', () => {
await authedPage
.getByRole('link', { name: targetWithTwoHosts.name })
.click();
await authedPage.getByRole('button', { name: 'Connect' }).click();
await expect(
authedPage.getByRole('heading', { name: 'Choose a Host' }),
).toBeVisible();
await authedPage.getByRole('button', { name: host }).click();
// eslint-disable-next-line playwright/no-conditional-in-test
if (host === 'Quick Connect') {
await authedPage.getByRole('button', { name: host }).click();
} else {
await authedPage
.getByRole('row')
.filter({ hasText: host })
.getByRole('button', { name: 'Connect' })
.click();
}
await expect(
authedPage.getByRole('heading', { name: 'Sessions' }),
@@ -1,75 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
{{#if @showModal}}
<Hds::Modal data-test-host-modal @onClose={{fn @toggleModal false}} as |M|>
<M.Header @tagline={{@target.displayName}}>
{{t 'resources.session.actions.host'}}
</M.Header>
<M.Body>
<ul class='host-list-container'>
{{#each @hosts as |host|}}
<li class='host-container'>
<button
data-test-host-connect
type='button'
{{on 'click' (fn @connect @target host)}}
>
<div class='host-info-container'>
<Hds::IconTile
@color='boundary'
@icon='network-alt'
@size='small'
/>
<div class='host-info'>
<Hds::Text::Body @tag='p' @weight='semibold' @color='primary'>
{{host.displayName}}
</Hds::Text::Body>
{{#if host.isStatic}}
<Hds::Text::Body @tag='p' @size='100' @color='faint'>
{{host.address}}
</Hds::Text::Body>
{{/if}}
{{#if host.isPlugin}}
{{#if host.isUnknown}}
<Hds::Badge
@text={{t 'resources.host-catalog.types.unknown'}}
/>
{{else}}
<Hds::Badge
@text={{t 'resources.host-catalog.types.plugin'}}
@icon={{host.plugin.name}}
/>
{{/if}}
{{/if}}
{{#if host.description}}
<Hds::Text::Body @tag='p' @size='100' @color='faint'>
{{host.description}}
</Hds::Text::Body>
{{/if}}
</div>
</div>
<Hds::Icon
class='host-arrow'
@name='arrow-right'
@isInline={{true}}
/>
</button>
</li>
{{/each}}
</ul>
</M.Body>
<M.Footer>
<Hds::Button
data-test-host-quick-connect
type='button'
@icon='entry-point'
@iconPosition='trailing'
@text={{t 'resources.session.actions.quick-connect'}}
{{on 'click' (fn @connect @target)}}
/>
</M.Footer>
</Hds::Modal>
{{/if}}
@@ -25,11 +25,6 @@ export default class ScopesScopeProjectsTargetsTargetController extends Controll
// =methods
@action
toggleModal(value) {
this.isConnecting = value;
}
/**
* Connect method that calls parent connect method and handles
* connection errors unique to this route
@@ -48,45 +43,9 @@ export default class ScopesScopeProjectsTargetsTargetController extends Controll
.then(() => this.connect(target, host))
.catch(() => {
// Reset the flag as this was user initiated and we're not
// in a transition or have a host modal open
// in a transition
this.isConnectionError = false;
});
}
}
/**
* Establish a session to current target.
* @param {TargetModel} target
* @param {HostModel} host
* hostConnect is only called when making a connection with a host and ensures that the host modal is automatically closed in the case of a connection error.
*/
@action
async hostConnect(target, host) {
await this.connect(target, host);
if (this.isConnectionError) {
this.toggleModal(false);
this.isConnectionError = false;
}
}
/**
* Determine if we show host modal or quick connect based on target attributes.
* @param {TargetModel} model
* @param {function} toggleModal
*/
@action
async preConnect(model) {
/**
* if hosts length is 1 or less we will try to
* connect, even if there is no address on the target and
* rely on the CLI to give the user the proper error or if
* there are 2 or more hosts we show the modal for host selection
*/
if (model.hosts.length <= 1) {
await this.connect(model.target);
} else {
this.toggleModal(true);
}
}
}
-37
View File
@@ -398,43 +398,6 @@
margin-top: 0.75rem;
}
// Host Modal
.host-list-container {
border: 1px solid var(--token-color-border-primary);
padding: 0.25rem;
border-radius: 6px;
}
.host-container {
button {
background: unset;
border: unset;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 0.5rem;
.host-info-container {
display: flex;
gap: 0.75rem;
.host-info {
text-align: left;
}
}
&:hover {
background-color: var(--token-color-page-faint);
cursor: pointer;
}
}
.host-arrow {
color: var(--token-color-foreground-faint);
}
}
// Unsupported controller modal
.unsupported-modal-body {
display: grid;
@@ -22,34 +22,73 @@
{{#if (can 'connect target' @model.target)}}
<Hds::Button
data-test-target-detail-connect-button
@text={{t 'resources.session.actions.connect'}}
@text={{if
(gt @model.hosts.length 1)
(t 'resources.session.actions.quick-connect')
(t 'resources.session.actions.connect')
}}
@icon='entry-point'
@iconPosition='trailing'
{{on 'click' (fn this.preConnect @model)}}
{{on 'click' (fn this.connect @model.target)}}
/>
{{/if}}
</page.actions>
<page.body class='details-screen-body'>
<Rose::Layout::BodyContent as |bc|>
<Target::HostModal
@showModal={{this.isConnecting}}
@toggleModal={{this.toggleModal}}
@connect={{this.hostConnect}}
@hosts={{@model.hosts}}
@target={{@model.target}}
/>
<bc.Body class='target-details-body'>
<Hds::ApplicationState as |A|>
<A.Header
@title={{t 'resources.target.messages.connect-more-info.title'}}
/>
<A.Body
@text={{t
'resources.target.messages.connect-more-info.description'
{{#if (gt @model.hosts.length 1)}}
<Hds::Table
@model={{@model.hosts}}
@columns={{array
(hash label=(t 'resources.target.host-source.title_plural'))
(hash label=(t 'form.address.label'))
(hash
label=(t 'titles.actions') isVisuallyHidden=true align='right'
)
}}
/>
</Hds::ApplicationState>
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
<Hds::Text::Body @weight='semibold' @tag='p'>
{{B.data.displayName}}
</Hds::Text::Body>
<Hds::Text::Body @tag='p'>
{{B.data.description}}
</Hds::Text::Body>
</B.Td>
<B.Td>
<Hds::Text::Body @tag='p'>
{{B.data.address}}
</Hds::Text::Body>
</B.Td>
<B.Td>
<Hds::Button
data-test-target-connect-button={{B.data.id}}
@text={{t 'resources.session.actions.connect'}}
@color='secondary'
{{on 'click' (fn this.connect @model.target B.data)}}
/>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{else}}
<Hds::ApplicationState as |A|>
<A.Header
@title={{t 'resources.target.messages.connect-more-info.title'}}
/>
<A.Body
@text={{t
'resources.target.messages.connect-more-info.description'
}}
/>
</Hds::ApplicationState>
{{/if}}
</bc.Body>
<bc.Sidebar>
{{#if @model.target.aliases}}
@@ -32,7 +32,6 @@ module('Acceptance | projects | targets | index', function (hooks) {
const TARGET_DETAILS_ROUTE_NAME =
'scopes.scope.projects.targets.target.index';
const HDS_DIALOG_MODAL = '.hds-modal';
const CHOOSE_HOST_MODAL = '[data-test-host-modal]';
const HDS_DIALOG_MODAL_BUTTONS = '.hds-modal__footer button';
const HDS_DIALOG_RETRY_BUTTON =
'.hds-modal__footer .hds-button--color-primary';
@@ -275,7 +274,6 @@ module('Acceptance | projects | targets | index', function (hooks) {
await click(`[data-test-targets-connect-button="${instances.target.id}"]`);
assert.dom(HDS_DIALOG_MODAL).isVisible();
assert.dom(CHOOSE_HOST_MODAL).doesNotExist();
assert.strictEqual(currentRouteName(), TARGET_DETAILS_ROUTE_NAME);
});
@@ -16,9 +16,12 @@ module('Acceptance | projects | targets | target', function (hooks) {
setupMirage(hooks);
setupStubs(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_CONNECT_BUTTON = '[data-test-target-detail-connect-button]';
const TARGET_QUICK_CONNECT_BUTTON = '[data-test-host-quick-connect]';
const TARGET_HOST_CONNECT_BUTTON = '[data-test-host-connect]';
const TARGET_HOST_SOURCE_CONNECT_BUTTON = (id) =>
`[data-test-target-connect-button=${id}]`;
const APP_STATE_TITLE = '.hds-application-state__title';
const HDS_DIALOG_MODAL = '.hds-modal';
const HDS_DIALOG_MODAL_BUTTONS = '.hds-modal__footer button';
@@ -228,8 +231,29 @@ module('Acceptance | projects | targets | target', function (hooks) {
assert.dom(APP_STATE_TITLE).hasText('Connected');
});
test('user can connect to a target with two hosts using host modal', async function (assert) {
assert.expect(3);
test('user can see host source table when visiting a target via resource link', async function (assert) {
const targetId = instances.targetWithTwoHosts.id;
await visit(urls.targets);
await click(TARGET_RESOURCE_LINK(targetId));
assert.dom('tbody tr').exists({ count: 2 });
});
test('user can see host source table when visiting a target via connect button', async function (assert) {
const targetId = instances.targetWithTwoHosts.id;
await visit(urls.targets);
await click(TARGET_TABLE_CONNECT_BUTTON(targetId));
assert.dom('tbody tr').exists({ count: 2 });
});
test('user can connect to a target with two hosts using host source table', async function (assert) {
const hostId =
instances.targetWithTwoHosts.hostSets.models[0].hosts.models[0].id;
assert.expect(2);
this.ipcStub.withArgs('cliExists').returns(true);
this.ipcStub.withArgs('connect').returns({
session_id: instances.session.id,
@@ -241,17 +265,35 @@ module('Acceptance | projects | targets | target', function (hooks) {
await click(`[href="${urls.targetWithTwoHosts}"]`);
assert.dom(APP_STATE_TITLE).hasText('Connect for more info');
assert.dom('tbody tr').exists({ count: 2 });
await click(TARGET_CONNECT_BUTTON);
assert.dom(TARGET_HOST_CONNECT_BUTTON).exists({ count: 2 });
await click(TARGET_QUICK_CONNECT_BUTTON);
await click(TARGET_HOST_SOURCE_CONNECT_BUTTON(hostId));
assert.dom(APP_STATE_TITLE).hasText('Connected');
});
test.each(
'user sees correct button text for a',
{
'target with address': { target: 'target', expectedText: 'Connect' },
'target with one host': {
target: 'targetWithOneHost',
expectedText: 'Connect',
},
'target with two hosts': {
target: 'targetWithTwoHosts',
expectedText: 'Quick Connect',
},
},
async function (assert, input) {
await visit(urls.targets);
await click(`[href="${urls[input.target]}"]`);
assert.dom(TARGET_CONNECT_BUTTON).hasText(input.expectedText);
},
);
test('user can visit target details screen without read permissions for host-set', async function (assert) {
assert.expect(1);
this.server.get('/host-sets/:id', () => new Response(403));
@@ -1,126 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
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';
module('Integration | Component | target/host-modal', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks, 'en-us');
let store;
let target;
let hostA;
let hostB;
let connect;
let toggleModal;
hooks.beforeEach(function () {
connect = () => {
this.set('called', true);
};
toggleModal = () => {
this.set('called', true);
};
store = this.owner.lookup('service:store');
target = store.createRecord('target', {
name: 'User',
});
hostA = store.createRecord('host', {
name: 'Host A',
compositeType: 'static',
address: '188e:68a9:b342:c05c:2595:2f46:499c:759f',
});
hostB = store.createRecord('host', {
name: 'Host B',
compositeType: 'static',
description: 'Description of host B',
address: '188e:68a9:b342:c05c:2595:499c:759f:2f46',
});
});
test('it renders', async function (assert) {
assert.expect(1);
this.set('connect', connect);
this.set('toggleModal', toggleModal);
this.set('hosts', [hostA, hostB]);
this.set('target', target);
await render(hbs`<Target::HostModal
@showModal={{true}}
@toggleModal={{this.toggleModal}}
@connect={{this.connect}}
@hosts={{this.hosts}}
@target={{this.target}} />`);
assert.dom('.hds-modal__tagline').hasText(target.displayName);
});
test('it closes model when dismissed', async function (assert) {
assert.expect(2);
const toggleModal = () => {
this.set('showModal', false);
};
this.set('connect', connect);
this.set('toggleModal', toggleModal);
this.set('showModal', true);
this.set('hosts', [hostA, hostB]);
this.set('target', target);
await render(hbs`<Target::HostModal
@showModal={{this.showModal}}
@toggleModal={{this.toggleModal}}
@connect={{this.connect}}
@hosts={{this.hosts}}
@target={{this.target}} />`);
assert.dom('.hds-modal__tagline').hasText(target.displayName);
await click('.hds-dismiss-button');
assert.dom('.hds-modal__tagline').doesNotExist();
});
test('it calls connect when Quick Connect button clicked', async function (assert) {
assert.expect(1);
this.set('connect', connect);
this.set('toggleModal', toggleModal);
this.set('hosts', [hostA, hostB]);
this.set('target', target);
await render(hbs`<Target::HostModal
@showModal={{true}}
@toggleModal={{this.toggleModal}}
@connect={{this.connect}}
@hosts={{this.hosts}}
@target={{this.target}} />`);
await click('[data-test-host-quick-connect]');
assert.true(this.called);
});
test('it calls connect when host item is clicked', async function (assert) {
assert.expect(1);
this.set('connect', connect);
this.set('toggleModal', toggleModal);
this.set('hosts', [hostA, hostB]);
this.set('target', target);
await render(hbs`<Target::HostModal
@showModal={{true}}
@toggleModal={{this.toggleModal}}
@connect={{this.connect}}
@hosts={{this.hosts}}
@target={{this.target}} />`);
await click('[data-test-host-connect]');
assert.true(this.called);
});
});