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:
1 parent
a747614a74
commit
96d7c055b0
8 files changed
+120
-316
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user