Icu 10315 add credentials panel (#1855)
* feat: 🎸 add credentials panel for session detail screen ✅ Closes: ICU-10315 * fix: 🐛 remove use of A in sessions list array * refactor: 💡 fix class name to apply style to both detail screen * refactor: 💡 add comment to resetController on session route * refactor: 💡 add @value to HDS Toggle in session detail page * refactor: 💡 class name refactor for credential panel * refactor: 💡 remove unneeded div around CodeEditor * refactor: 💡 change border units from rem to px * feat: 🎸 create CredentialsPanel component * Update ui/desktop/tests/integration/components/credentials-panel-test.js Co-authored-by: Dharini Jeeva <dharini@hashicorp.com> * refactor: 💡 update credential panel styles * style: 💄 fix lint error for desktop styles * chore: 🤖 yarn.lock update --------- Co-authored-by: Dharini Jeeva <dharini@hashicorp.com>
This commit is contained in:
1 parent
b300e898ff
commit
d5d2045fb5
11 files changed
+500
-12
No files matched your search
@@ -73,7 +73,7 @@ export default function initializeMockIPC(server, config) {
|
||||
{
|
||||
credential_source: {
|
||||
id: 'clvlt_4cvscMTl0N',
|
||||
name: 'Source Name',
|
||||
name: 'Credential Library 1',
|
||||
description: 'Source Description',
|
||||
credential_store_id: 'csvlt_Q1HFGt7Jpm',
|
||||
type: 'vault',
|
||||
@@ -116,7 +116,7 @@ export default function initializeMockIPC(server, config) {
|
||||
{
|
||||
credential_source: {
|
||||
id: 'clvlt_4cvscMTl0N',
|
||||
name: 'Source Name',
|
||||
name: 'Credential Library 2',
|
||||
description: 'Source Description',
|
||||
credential_store_id: 'csvlt_Q1HFGt7Jpm',
|
||||
type: 'vault',
|
||||
@@ -129,7 +129,7 @@ export default function initializeMockIPC(server, config) {
|
||||
{
|
||||
credential_source: {
|
||||
id: 'clvlt_9KWscxpcY7',
|
||||
name: 'Source Name',
|
||||
name: 'Credential Library 3',
|
||||
description: 'Source Description',
|
||||
credential_store_id: 'csvlt_Q1HFGt7Jpm',
|
||||
type: 'vault',
|
||||
@@ -141,7 +141,7 @@ export default function initializeMockIPC(server, config) {
|
||||
{
|
||||
credential_source: {
|
||||
id: 'clvlt_9KWscxpcY7',
|
||||
name: 'Source Name',
|
||||
name: 'Credential Library 4',
|
||||
description: 'Source Description',
|
||||
credential_store_id: 'csvlt_Q1HFGt7Jpm',
|
||||
type: 'vault',
|
||||
|
||||
@@ -288,20 +288,24 @@ session:
|
||||
none-active-friendly:
|
||||
title: No Active Sessions Available
|
||||
description: No active sessions available to display yet.
|
||||
connected:
|
||||
title: Connected
|
||||
description: You can now access {targetDisplayName}
|
||||
actions:
|
||||
connect: Connect
|
||||
end: End session
|
||||
credential:
|
||||
title: Credential
|
||||
title_plural: Credentials
|
||||
description: These credentials have been provided for services on this server.
|
||||
secret:
|
||||
label: Secret
|
||||
credential_source:
|
||||
title: Credential Source
|
||||
actions:
|
||||
raw-api:
|
||||
show: View raw API output
|
||||
hide: Hide raw API output
|
||||
show: View API output
|
||||
hide: Hide API output
|
||||
org:
|
||||
title: Org
|
||||
title_plural: Orgs
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: MPL-2.0
|
||||
}}
|
||||
|
||||
<div class='credential-panel-header'>
|
||||
<div class='title hds-typography-display-400'>
|
||||
{{t 'resources.session.credential.title_plural'}}
|
||||
</div>
|
||||
<Hds::Form::Toggle::Field
|
||||
data-test-toggle-credentials
|
||||
@value={{this.isRawApiVisible}}
|
||||
{{on 'change' this.toggleCredentials}}
|
||||
as |F|
|
||||
>
|
||||
<F.Label>
|
||||
{{t 'resources.session.credential.actions.raw-api.show'}}
|
||||
</F.Label>
|
||||
</Hds::Form::Toggle::Field>
|
||||
</div>
|
||||
<div class='hds-typography-body-200'>
|
||||
{{t 'resources.session.credential.description'}}
|
||||
</div>
|
||||
{{#each @credentials as |credential|}}
|
||||
{{#if this.isRawApiVisible}}
|
||||
<div class='raw-credential-panel-body'>
|
||||
<div class='raw-credential-header'>
|
||||
<div class='raw-credential-title'>
|
||||
<div class='credential-name hds-typography-display-200'>
|
||||
{{#if credential.source.name}}
|
||||
{{credential.source.name}}
|
||||
{{else}}
|
||||
{{credential.source.id}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<Hds::Copy::Button
|
||||
@text='Copy'
|
||||
@isIconOnly={{true}}
|
||||
@textToCopy={{raw-json credential.rawCredential}}
|
||||
/>
|
||||
</div>
|
||||
<div class='hds-typography-body-100'>
|
||||
{{credential.source.description}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Rose::CodeEditor @codeValue={{raw-json credential.rawCredential}} as |c|>
|
||||
<c.fieldEditor
|
||||
class='rose-autosize raw-secret'
|
||||
@options={{hash
|
||||
mode='application/json'
|
||||
readOnly=true
|
||||
lineNumbers=false
|
||||
cursorBlinkRate=-1
|
||||
styleActiveLine=false
|
||||
autoRefresh=true
|
||||
}}
|
||||
/>
|
||||
</Rose::CodeEditor>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class='credential-panel-body'>
|
||||
<div class='credential-header'>
|
||||
<div class='credential-name hds-typography-display-200'>
|
||||
{{#if credential.source.name}}
|
||||
{{credential.source.name}}
|
||||
{{else}}
|
||||
{{credential.source.id}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class='hds-typography-body-100'>
|
||||
{{credential.source.description}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class='credential-secret'>
|
||||
{{#if credential.secrets}}
|
||||
{{#each credential.secrets as |secret|}}
|
||||
<li class='secret-container'>
|
||||
<span class='secret-key'>{{secret.key}}</span>
|
||||
<HiddenSecret @secret={{secret.value}} />
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import Component from '@glimmer/component';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import { action } from '@ember/object';
|
||||
|
||||
export default class CredentialsPanelIndexComponent extends Component {
|
||||
@tracked isRawApiVisible = false;
|
||||
|
||||
@action
|
||||
toggleCredentials() {
|
||||
this.isRawApiVisible = !this.isRawApiVisible;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ import { inject as service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { notifySuccess, notifyError } from 'core/decorators/notify';
|
||||
import { resourceFilter } from 'core/decorators/resource-filter';
|
||||
import { A } from '@ember/array';
|
||||
|
||||
export default class ScopesScopeProjectsSessionsRoute extends Route {
|
||||
// =services
|
||||
@@ -79,7 +78,7 @@ export default class ScopesScopeProjectsSessionsRoute extends Route {
|
||||
sessions = sessions.filter((s) => this.status.includes(s?.status));
|
||||
}
|
||||
|
||||
return A(sessions);
|
||||
return sessions;
|
||||
}
|
||||
|
||||
// =actions
|
||||
|
||||
@@ -530,3 +530,89 @@ $chevron-down: url("data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231
|
||||
background-color: var(--token-color-palette-neutral-50);
|
||||
}
|
||||
}
|
||||
|
||||
.details-screen-body {
|
||||
.rose-layout-body-content {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Credentials Panel
|
||||
.credential-panel-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.title {
|
||||
font-weight: var(--token-typography-font-weight-bold);
|
||||
}
|
||||
|
||||
/* stylelint-disable selector-class-pattern */
|
||||
.hds-form-field--layout-flag {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.credential-panel-body {
|
||||
border: 1px solid var(--token-color-border-primary);
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
margin-top: 0.75rem;
|
||||
|
||||
.credential-header {
|
||||
padding: 1.5rem;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.credential-secret {
|
||||
width: 66%;
|
||||
margin: 0.75rem;
|
||||
|
||||
.secret-container {
|
||||
margin: 0.75rem 0.5rem;
|
||||
border-bottom: 1px solid var(--token-color-border-primary);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.secret-key {
|
||||
font-weight: var(--token-typography-font-weight-semibold);
|
||||
}
|
||||
|
||||
.hidden-secret {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rose-button-ghost {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.credential-name {
|
||||
font-weight: var(--token-typography-font-weight-semibold);
|
||||
}
|
||||
|
||||
.raw-credential-panel-body {
|
||||
display: block;
|
||||
margin-top: 0.75rem;
|
||||
|
||||
.raw-credential-header {
|
||||
border: 1px solid var(--token-color-border-primary);
|
||||
border-radius: 5px 5px 0 0;
|
||||
border-bottom: none;
|
||||
padding: 1.5rem 1.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.raw-credential-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.raw-secret .CodeMirror {
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
<page.actions>
|
||||
<Hds::Button
|
||||
data-test-session-detail-cancel-button
|
||||
@text={{t 'resources.session.actions.end'}}
|
||||
@icon='x'
|
||||
@color='secondary'
|
||||
@@ -22,11 +23,25 @@
|
||||
/>
|
||||
</page.actions>
|
||||
|
||||
<page.body>
|
||||
<page.body class='details-screen-body'>
|
||||
<div class='copy-url-container'>{{t 'actions.copy-to-clipboard'}}</div>
|
||||
<Rose::Layout::BodyContent as |bc|>
|
||||
<bc.Body class='session-details-body'>
|
||||
{{t 'resources.session.description'}}
|
||||
<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>
|
||||
<bc.Sidebar>
|
||||
<div class='details-sidebar-target-details'>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
<page.actions>
|
||||
<Hds::Button
|
||||
data-test-target-detail-connect-button
|
||||
@text={{t 'resources.session.actions.connect'}}
|
||||
@icon='entry-point'
|
||||
@iconPosition='trailing'
|
||||
@@ -23,7 +24,7 @@
|
||||
/>
|
||||
</page.actions>
|
||||
|
||||
<page.body>
|
||||
<page.body class='details-screen-body'>
|
||||
<Rose::Layout::BodyContent as |bc|>
|
||||
<bc.Body class='target-details-body'>
|
||||
<Hds::ApplicationState as |A|>
|
||||
|
||||
File renamed without changes.
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { visit, currentURL, click } from '@ember/test-helpers';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
import { Response } from 'miragejs';
|
||||
import a11yAudit from 'ember-a11y-testing/test-support/audit';
|
||||
import { authenticateSession } from 'ember-simple-auth/test-support';
|
||||
import sinon from 'sinon';
|
||||
import { STATUS_SESSION_ACTIVE } from 'api/models/session';
|
||||
|
||||
module('Acceptance | projects | sessions | session', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
let mockIPC;
|
||||
let messageHandler;
|
||||
|
||||
const instances = {
|
||||
scopes: {
|
||||
global: null,
|
||||
org: null,
|
||||
project: null,
|
||||
},
|
||||
authMethods: {
|
||||
global: null,
|
||||
},
|
||||
user: null,
|
||||
session: null,
|
||||
};
|
||||
|
||||
const stubs = {
|
||||
global: null,
|
||||
org: null,
|
||||
ipcService: null,
|
||||
};
|
||||
|
||||
const urls = {
|
||||
index: '/',
|
||||
clusterUrl: '/cluster-url',
|
||||
scopes: {
|
||||
global: null,
|
||||
org: null,
|
||||
},
|
||||
authenticate: {
|
||||
global: null,
|
||||
methods: {
|
||||
global: null,
|
||||
},
|
||||
},
|
||||
projects: null,
|
||||
target: null,
|
||||
sessions: null,
|
||||
session: null,
|
||||
};
|
||||
|
||||
const setDefaultClusterUrl = (test) => {
|
||||
const windowOrigin = window.location.origin;
|
||||
const clusterUrl = test.owner.lookup('service:clusterUrl');
|
||||
clusterUrl.rendererClusterUrl = windowOrigin;
|
||||
};
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
instances.user = this.server.create('user', {
|
||||
scope: instances.scopes.global,
|
||||
});
|
||||
|
||||
authenticateSession({ user_id: instances.user.id });
|
||||
|
||||
// create scopes
|
||||
instances.scopes.global = this.server.create('scope', { id: 'global' });
|
||||
stubs.global = { id: 'global', type: 'global' };
|
||||
instances.scopes.org = this.server.create('scope', {
|
||||
type: 'org',
|
||||
scope: stubs.global,
|
||||
});
|
||||
stubs.org = { id: instances.scopes.org.id, type: 'org' };
|
||||
instances.scopes.project = this.server.create('scope', {
|
||||
type: 'project',
|
||||
scope: stubs.org,
|
||||
});
|
||||
stubs.project = { id: instances.scopes.project.id, type: 'project' };
|
||||
|
||||
instances.authMethods.global = this.server.create('auth-method', {
|
||||
scope: instances.scopes.global,
|
||||
});
|
||||
instances.target = this.server.create(
|
||||
'target',
|
||||
{ scope: instances.scopes.project },
|
||||
'withAssociations'
|
||||
);
|
||||
instances.session = this.server.create(
|
||||
'session',
|
||||
{
|
||||
scope: instances.scopes.project,
|
||||
target: instances.target,
|
||||
status: STATUS_SESSION_ACTIVE,
|
||||
user: instances.user,
|
||||
},
|
||||
'withAssociations'
|
||||
);
|
||||
|
||||
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
|
||||
urls.scopes.org = `/scopes/${instances.scopes.org.id}`;
|
||||
urls.authenticate.global = `${urls.scopes.global}/authenticate`;
|
||||
urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`;
|
||||
urls.projects = `${urls.scopes.org}/projects`;
|
||||
urls.target = `${urls.projects}/targets/${instances.target.id}`;
|
||||
urls.sessions = `${urls.projects}/sessions`;
|
||||
urls.session = `${urls.projects}/sessions/${instances.session.id}`;
|
||||
|
||||
class MockIPC {
|
||||
clusterUrl = null;
|
||||
|
||||
invoke(method, payload) {
|
||||
return this[method](payload);
|
||||
}
|
||||
|
||||
getClusterUrl() {
|
||||
return this.clusterUrl;
|
||||
}
|
||||
|
||||
setClusterUrl(clusterUrl) {
|
||||
this.clusterUrl = clusterUrl;
|
||||
return this.clusterUrl;
|
||||
}
|
||||
}
|
||||
|
||||
mockIPC = new MockIPC();
|
||||
messageHandler = async function (event) {
|
||||
if (event.origin !== window.location.origin) return;
|
||||
const { method, payload } = event.data;
|
||||
if (method) {
|
||||
const response = await mockIPC.invoke(method, payload);
|
||||
event.ports[0].postMessage(response);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('message', messageHandler);
|
||||
setDefaultClusterUrl(this);
|
||||
|
||||
const ipcService = this.owner.lookup('service:ipc');
|
||||
stubs.ipcService = sinon.stub(ipcService, 'invoke');
|
||||
});
|
||||
|
||||
hooks.afterEach(function () {
|
||||
window.removeEventListener('message', messageHandler);
|
||||
});
|
||||
|
||||
test('visiting session detail', async function (assert) {
|
||||
assert.expect(1);
|
||||
|
||||
await visit(urls.session);
|
||||
await a11yAudit();
|
||||
|
||||
assert.strictEqual(currentURL(), urls.session);
|
||||
});
|
||||
|
||||
test('visiting session with no credentials', async function (assert) {
|
||||
assert.expect(4);
|
||||
stubs.ipcService.withArgs('cliExists').returns(true);
|
||||
stubs.ipcService.withArgs('connect').returns({
|
||||
session_id: instances.session.id,
|
||||
address: 'a_123',
|
||||
port: 'p_123',
|
||||
protocol: 'tcp',
|
||||
});
|
||||
|
||||
await visit(urls.target);
|
||||
await click('[data-test-target-detail-connect-button]');
|
||||
|
||||
assert.strictEqual(currentURL(), urls.session);
|
||||
assert.dom('credential-panel-header').doesNotExist();
|
||||
assert.dom('credential-panel-body').doesNotExist();
|
||||
assert
|
||||
.dom('[data-test-no-credentials]')
|
||||
.hasText(`Connected You can now access ${instances.target.name}`);
|
||||
});
|
||||
|
||||
test('cancelling a session', async function (assert) {
|
||||
assert.expect(1);
|
||||
stubs.ipcService.withArgs('stop');
|
||||
|
||||
await visit(urls.session);
|
||||
await click('[data-test-session-detail-cancel-button]');
|
||||
|
||||
assert.dom('[role="alert"].is-success').isVisible();
|
||||
});
|
||||
|
||||
test('cancelling a session with error shows notification', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.server.post('/sessions/:id_method', () => new Response(400));
|
||||
|
||||
await visit(urls.session);
|
||||
await click('[data-test-session-detail-cancel-button]');
|
||||
|
||||
assert.dom('[role="alert"].is-error').isVisible();
|
||||
});
|
||||
|
||||
test('cancelling a session with ipc error shows notification', async function (assert) {
|
||||
assert.expect(1);
|
||||
stubs.ipcService.withArgs('stop').throws();
|
||||
|
||||
await visit(urls.session);
|
||||
await click('[data-test-session-detail-cancel-button]');
|
||||
|
||||
assert.dom('[role="alert"].is-error').isVisible();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: MPL-2.0
|
||||
*/
|
||||
|
||||
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 | credentials-panel', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupIntl(hooks);
|
||||
|
||||
const credential = {
|
||||
rawCredential: {
|
||||
credential_source: {
|
||||
id: 'clvlt_4cvscMTl0N',
|
||||
name: 'Credential Library 1',
|
||||
description: 'Source Description',
|
||||
credential_store_id: 'csvlt_Q1HFGt7Jpm',
|
||||
type: 'vault',
|
||||
},
|
||||
secret: {
|
||||
raw: 'anVzdC1hLXJhbmRvbS1zdHJpbmc=',
|
||||
},
|
||||
},
|
||||
source: {
|
||||
id: 'clvlt_4cvscMTl0N',
|
||||
name: 'Credential Library 1',
|
||||
description: 'Source Description',
|
||||
type: 'vault',
|
||||
},
|
||||
secrets: [
|
||||
{
|
||||
key: 'secret',
|
||||
value: 'just-a-random-string',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.set('credentials', [credential]);
|
||||
|
||||
await render(hbs`
|
||||
<CredentialsPanel @credentials={{this.credentials}} />
|
||||
`);
|
||||
|
||||
assert.dom('.credential-name').hasText('Credential Library 1');
|
||||
});
|
||||
|
||||
test('it shows code editor when toggle clicked', async function (assert) {
|
||||
assert.expect(1);
|
||||
this.set('credentials', [credential]);
|
||||
await render(hbs`
|
||||
<CredentialsPanel @credentials={{this.credentials}} />
|
||||
`);
|
||||
|
||||
await click('[data-test-toggle-credentials]');
|
||||
|
||||
assert.dom('.raw-secret').isVisible();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user