diff --git a/addons/api/mirage/scenarios/ipc.js b/addons/api/mirage/scenarios/ipc.js index 6e2cd299b..7001bc831 100644 --- a/addons/api/mirage/scenarios/ipc.js +++ b/addons/api/mirage/scenarios/ipc.js @@ -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', diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml index 538830480..91baf4f81 100644 --- a/addons/core/translations/resources/en-us.yaml +++ b/addons/core/translations/resources/en-us.yaml @@ -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 diff --git a/ui/desktop/app/components/credentials-panel/index.hbs b/ui/desktop/app/components/credentials-panel/index.hbs new file mode 100644 index 000000000..43ed5e6fd --- /dev/null +++ b/ui/desktop/app/components/credentials-panel/index.hbs @@ -0,0 +1,88 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: MPL-2.0 +}} + +
+
+ {{t 'resources.session.credential.title_plural'}} +
+ + + {{t 'resources.session.credential.actions.raw-api.show'}} + + +
+
+ {{t 'resources.session.credential.description'}} +
+{{#each @credentials as |credential|}} + {{#if this.isRawApiVisible}} +
+
+
+
+ {{#if credential.source.name}} + {{credential.source.name}} + {{else}} + {{credential.source.id}} + {{/if}} +
+ +
+
+ {{credential.source.description}} +
+
+ + + + +
+ {{else}} +
+
+
+ {{#if credential.source.name}} + {{credential.source.name}} + {{else}} + {{credential.source.id}} + {{/if}} +
+
+ {{credential.source.description}} +
+
+ + +
+ {{/if}} +{{/each}} \ No newline at end of file diff --git a/ui/desktop/app/components/credentials-panel/index.js b/ui/desktop/app/components/credentials-panel/index.js new file mode 100644 index 000000000..9d8328a27 --- /dev/null +++ b/ui/desktop/app/components/credentials-panel/index.js @@ -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; + } +} diff --git a/ui/desktop/app/routes/scopes/scope/projects/sessions.js b/ui/desktop/app/routes/scopes/scope/projects/sessions.js index fda44b87c..a9e0032d7 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/sessions.js +++ b/ui/desktop/app/routes/scopes/scope/projects/sessions.js @@ -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 diff --git a/ui/desktop/app/styles/app.scss b/ui/desktop/app/styles/app.scss index dc98f392c..99a977929 100644 --- a/ui/desktop/app/styles/app.scss +++ b/ui/desktop/app/styles/app.scss @@ -530,3 +530,89 @@ $chevron-down: url("data:image/svg+xml;utf-8, - +
{{t 'actions.copy-to-clipboard'}}
- - {{t 'resources.session.description'}} + + {{#if @model.credentials}} + + {{else}} + + + + + {{/if}}
diff --git a/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs b/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs index d2b4d415e..70349e4f4 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/targets/target.hbs @@ -16,6 +16,7 @@ - + diff --git a/ui/desktop/tests/acceptance/projects/sessions-test.js b/ui/desktop/tests/acceptance/projects/sessions/index-test.js similarity index 100% rename from ui/desktop/tests/acceptance/projects/sessions-test.js rename to ui/desktop/tests/acceptance/projects/sessions/index-test.js diff --git a/ui/desktop/tests/acceptance/projects/sessions/session-test.js b/ui/desktop/tests/acceptance/projects/sessions/session-test.js new file mode 100644 index 000000000..1001fbdca --- /dev/null +++ b/ui/desktop/tests/acceptance/projects/sessions/session-test.js @@ -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(); + }); +}); diff --git a/ui/desktop/tests/integration/components/credentials-panel-test.js b/ui/desktop/tests/integration/components/credentials-panel-test.js new file mode 100644 index 000000000..a0a9a4d9b --- /dev/null +++ b/ui/desktop/tests/integration/components/credentials-panel-test.js @@ -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` + + `); + + 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` + + `); + + await click('[data-test-toggle-credentials]'); + + assert.dom('.raw-secret').isVisible(); + }); +});