refactor: 💡 Adopt HDS copy snippet Desktop UI (#2146)

* refactor: 💡 Adopt HDS copy snippet Desktop UI

Adopt HDS copy snippet and delete usage of Copyable custom component
This commit is contained in:
Carlos Alcaide Corvo authored and GitHub committed 2024-02-22 13:56:56 -08:00
1 parent 912be13744
commit 956f6fd04d
16 files changed
+113 -328

No files matched your search

+2
View File
@@ -48,6 +48,8 @@ session_max_seconds:
session_connection_limit:
label: Maximum Connections
help: The maximum number of connections allowed per session. For unlimited, specify "-1".
session_id:
label: Session ID
worker_release_version:
label: Release Version
help: The version of the Boundary binary the worker is running.
@@ -1,35 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
<span
id={{this.id}}
class='copyable'
{{did-insert this.register}}
{{will-destroy this.tearDown}}
>
{{#if this.copied}}
<mark class='copyable-content'>
{{yield}}
</mark>
{{else}}
<span class='copyable-content'>
{{yield}}
</span>
{{/if}}
{{#if this.isClipboardLibrarySupported}}
<Rose::Button
id={{this.copyableButtonId}}
aria-describedby={{this.id}}
class='copyable-button'
@style='ghost'
@iconOnly={{if this.copied this.successIcon this.actionIcon}}
title={{if this.copied this.successText this.actionText}}
>
{{this.actionText}}
</Rose::Button>
{{/if}}
</span>
@@ -1,88 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Component from '@glimmer/component';
import ClipboardJS from 'clipboard';
import { action } from '@ember/object';
import { getOwner } from '@ember/application';
import { generateComponentID } from 'rose/utilities/component-auto-id';
import { tracked } from '@glimmer/tracking';
import { later } from '@ember/runloop';
export default class CopyableComponent extends Component {
// =attributes
id = generateComponentID();
copyableButtonId = `copyable-button-${this.id}`;
actionIcon = 'flight-icons/svg/clipboard-copy-16';
successIcon = 'flight-icons/svg/clipboard-checked-16';
actionText = this.args.buttonText;
successText = this.args.acknowledgeText;
@tracked copied = false;
clipboard = null;
confirmCopy() {
this.copied = true;
later(() => (this.copied = false), 1000);
}
/**
* Checks for ClipboardJS support.
* @return {boolean}
*/
get isClipboardLibrarySupported() {
return ClipboardJS.isSupported();
}
// =methods
/**
* Returns the first element with the specified class name if exists,
* otherwise undefined.
* @return {?HTMLElement}
*/
getFirstElementByClassName(className) {
const document = getOwner(this).lookup('service:-document').documentElement;
return document.getElementsByClassName(className)[0];
}
// =actions
/**
* Initialize ClipboardJS library and register for it's events.
*/
@action
register() {
this.tearDown();
/* istanbul ignore next */
this.clipboard = new ClipboardJS(`#${this.copyableButtonId}`, {
text: () => this.args.text,
// ClipboardJS depends on inserting a hidden textarea that receives
// focus in order to copy text to the clipboard. However, our modal
// has a focus trap that prevents this focus. Thus we try to set the
// container to a modal (if exists), since this is the only container
// a user would be able to interact with. If a modal doesn't exist,
// container is set to `undefined` and thus uses the default body element.
container: this.getFirstElementByClassName('rose-dialog'),
});
/* istanbul ignore next */
this.clipboard.on('success', (clipboardEvent) => {
clipboardEvent.clearSelection();
this.confirmCopy();
});
}
/**
* Destroy ClipboardJS library instance and cancel icon timer.
*/
@action
tearDown() {
this.clipboard?.destroy();
this.clipboard = null;
}
}
@@ -4,26 +4,25 @@
}}
<div class='hidden-secret'>
<Rose::Button
@style='ghost'
@iconOnly={{if
this.isHidden
'flight-icons/svg/eye-16'
'flight-icons/svg/eye-off-16'
}}
<Hds::Button
@text={{t 'actions.toggle-secret'}}
@icon={{if this.isHidden 'eye' 'eye-off'}}
@isIconOnly={{true}}
@color='secondary'
{{on 'click' this.toggleSecretVisibility}}
>
{{t 'actions.toggle-secret'}}
</Rose::Button>
<Copyable
@text={{this.displaySecretValue}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
/>
<div class='secret-content'>
{{#if this.isHidden}}
<pre>{{this.displaySecretValue}}</pre>
{{else}}
<span class='secret-mask'>{{this.textMask}}</span>
{{/if}}
</Copyable>
</div>
<Hds::Copy::Button
@text={{t 'actions.copy-to-clipboard'}}
@isIconOnly={{true}}
@textToCopy={{this.displaySecretValue}}
/>
</div>
@@ -65,13 +65,10 @@
{{/if}}
{{! Host ID }}
<list.Item @icon='hash' @color='faint'>
<Copyable
@text={{@model.host.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
{{@model.host.id}}
</Copyable>
<Hds::Copy::Snippet
@textToCopy={{@model.host.id}}
@color='secondary'
/>
</list.Item>
</Rose::MetadataList>
{{/if}}
@@ -120,13 +117,10 @@
{{/if}}
{{! Target ID }}
<list.Item @icon='hash' @color='faint'>
<Copyable
@text={{@model.target.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
{{@model.target.id}}
</Copyable>
<Hds::Copy::Snippet
@textToCopy={{@model.target.id}}
@color='secondary'
/>
</list.Item>
</Rose::MetadataList>
{{/if}}
+17 -58
View File
@@ -208,32 +208,6 @@
}
}
.copyable {
white-space: nowrap;
.copyable-button {
margin: 0;
padding: 0 sizing.rems(xxs);
top: -2px;
}
mark {
animation: fadeBackground 0.75s linear;
background-color: transparent;
color: inherit;
@keyframes fadeBackground {
0% {
background-color: var(--action-subtler-1);
}
100% {
background-color: transparent;
}
}
}
}
[class*='session-status-'] {
color: var(--ui-gray-subtler-2);
}
@@ -446,12 +420,6 @@ $chevron-down: url("data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231
border-top: sizing.rems(xxxxs) solid var(--ui-gray-subtler-3);
padding: sizing.rems(s);
}
// Allow text wrapping for copyable content in table
.rose-table .copyable {
white-space: inherit;
word-break: break-word;
}
}
// Hidden secret component (app/components/hidden-secret)
@@ -459,34 +427,21 @@ $chevron-down: url("data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231
display: flex;
align-items: center;
.copyable {
display: flex;
width: 100%;
justify-content: space-between;
.copyable-content {
display: flex;
align-items: center;
pre {
white-space: pre-wrap;
line-height: normal;
}
}
.secret-mask {
color: var(--ui-gray-subtler-2);
}
.rose-button-ghost {
// Copyable button icon.
color: var(--ui-gray-subtler-1);
}
.secret-content {
display: flex;
width: 100%;
justify-content: space-between;
align-items: center;
margin-left: 0.75rem;
}
.rose-button-ghost {
// Hide/Show secret button icon.
color: var(--ui-gray);
pre {
white-space: pre-wrap;
line-height: normal;
}
}
@@ -540,10 +495,6 @@ $chevron-down: url("data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231
text-transform: capitalize;
}
.copyable-button {
background-color: var(--token-color-palette-neutral-50);
}
.flight-icon {
flex-shrink: 0;
}
@@ -695,3 +646,11 @@ $chevron-down: url("data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231
display: flex;
gap: 1rem;
}
.link-copy-button-container {
display: flex;
flex-direction: row;
align-items: baseline;
gap: 0.5rem;
width: inherit;
}
@@ -65,43 +65,44 @@
<Hds::Table
@model={{this.sortedSessions}}
@columns={{array
(hash label=(t 'form.id.label'))
(hash label=(t 'form.started.label'))
(hash label=(t 'form.session_id.label'))
(hash label=(t 'resources.target.title'))
(hash label=(t 'form.proxy_url.label'))
(hash label=(t 'form.started.label'))
(hash label=(t 'form.status.label'))
(hash label='')
(hash label='' align='right' width='60px')
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso-human B.data.created_time}}
</time>
</B.Td>
<B.Td>
<div class='link-copy-button-container'>
{{#if B.data.isAvailable}}
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<LinkTo
<Hds::Link::Inline
@route='scopes.scope.projects.sessions.session.index'
@model={{B.data.id}}
data-test-session-detail-link={{B.data.id}}
>
<Hds::Text::Code>{{B.data.id}}</Hds::Text::Code>
</LinkTo>
</Copyable>
{{B.data.id}}
</Hds::Link::Inline>
{{else}}
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code
data-test-session-id-copy={{B.data.id}}
>{{B.data.id}}</Hds::Text::Code>
</Copyable>
<Hds::Text::Body data-test-session-id-copy={{B.data.id}}>
{{B.data.id}}
</Hds::Text::Body>
{{/if}}
<Hds::Copy::Button
@text={{B.data.id}}
@isIconOnly={{true}}
@textToCopy={{B.data.id}}
data-test-session-id-copy={{B.data.id}}
/>
</div>
</B.Td>
<B.Td>
{{B.data.target.displayName}}
@@ -120,28 +121,22 @@
<B.Td>
{{#if B.data.isAvailable}}
{{#if B.data.proxy}}
<Copyable
@text={{B.data.proxy}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code>{{B.data.proxy}}</Hds::Text::Code>
</Copyable>
<Hds::Copy::Snippet
@textToCopy={{B.data.proxy}}
@color='secondary'
/>
{{/if}}
{{/if}}
</B.Td>
<B.Td>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso-human B.data.created_time}}
</time>
</B.Td>
<B.Td>
<SessionStatus @model={{B.data}} />
</B.Td>
<B.Td>
<B.Td @align='right' width='60px'>
{{#if (can 'cancel session' B.data)}}
<Hds::Button
@text={{t 'actions.cancel'}}
@icon='x'
@isIconOnly={{true}}
@color='secondary'
{{on 'click' (route-action 'cancelSession' B.data)}}
data-test-session-cancel-button={{B.data.id}}
@@ -76,7 +76,7 @@
(hash label=(t 'resources.target.titles.active-sessions'))
(hash label=(t 'form.type.label'))
(hash label=(t 'resources.project.title'))
(hash label='')
(hash label='' align='right')
}}
@valign='middle'
>
@@ -102,13 +102,7 @@
</span>
</B.Td>
<B.Td>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code>{{B.data.id}}</Hds::Text::Code>
</Copyable>
<Hds::Copy::Snippet @textToCopy={{B.data.id}} @color='secondary' />
</B.Td>
<B.Td>
{{#if B.data.isActive}}
@@ -130,21 +124,18 @@
</B.Td>
<B.Td>
<div>
<Copyable
@text={{B.data.project.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<Hds::Text::Code
<Hds::Copy::Snippet
@textToCopy={{B.data.project.id}}
@color='secondary'
data-test-target-project-id={{B.data.project.id}}
>{{B.data.project.id}}</Hds::Text::Code>
</Copyable>
/>
</div>
{{#if B.data.project.name}}
<Hds::Badge @color='neutral' @text={{B.data.project.name}} />
{{/if}}
</B.Td>
<B.Td>
<B.Td @align='right'>
<div {{style display='inline-block'}}>
{{#if (can 'connect target' B.data)}}
{{#if (can 'read target' B.data)}}
<Hds::Button
@@ -164,6 +155,7 @@
/>
{{/if}}
{{/if}}
</div>
</B.Td>
</B.Tr>
</:body>
@@ -201,6 +193,7 @@
{{#if (and this.selectedTarget this.selectedTarget.isActive)}}
<Hds::Flyout
@onClose={{fn this.selectTarget null}}
@size='large'
class='flyout-with-table'
data-test-targets-sessions-flyout
as |M|
@@ -215,39 +208,41 @@
<Hds::Table
@model={{this.sortedTargetSessions}}
@columns={{array
(hash label=(t 'form.id.label'))
(hash label=(t 'form.started.label'))
(hash label=(t 'form.session_id.label'))
(hash label=(t 'form.proxy_url.label'))
(hash label='')
(hash label='' align='right' width='60px')
}}
@valign='middle'
>
<:body as |B|>
<B.Tr>
<B.Td>
<Copyable
@text={{B.data.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
<LinkTo
@route='scopes.scope.projects.sessions.session.index'
@model={{B.data.id}}
data-test-targets-session-detail-link={{B.data.id}}
>
<Hds::Text::Code>{{B.data.id}}</Hds::Text::Code>
</LinkTo>
</Copyable>
</B.Td>
<B.Td>
<time datetime={{format-date-iso B.data.created_time}}>
{{format-date-iso-human B.data.created_time}}
</time>
</B.Td>
<B.Td>
{{B.data.proxy}}
<div class='link-copy-button-container'>
<Hds::Link::Inline
@route='scopes.scope.projects.sessions.session.index'
@model={{B.data.id}}
data-test-targets-session-detail-link={{B.data.id}}
>
{{B.data.id}}
</Hds::Link::Inline>
<Hds::Copy::Button
@text={{B.data.id}}
@isIconOnly={{true}}
@textToCopy={{B.data.id}}
/>
</div>
</B.Td>
<B.Td>
{{B.data.proxy}}
</B.Td>
<B.Td @align='right' width='60px'>
{{#if (can 'cancel session' B.data)}}
<Hds::Button
data-test-session-flyout-cancel-button={{B.data.id}}
@@ -90,13 +90,10 @@
{{/if}}
{{! Target ID }}
<list.Item @icon='hash' @color='faint'>
<Copyable
@text={{@model.target.id}}
@buttonText={{t 'actions.copy-to-clipboard'}}
@acknowledgeText={{t 'states.copied'}}
>
{{@model.target.id}}
</Copyable>
<Hds::Copy::Snippet
@textToCopy={{@model.target.id}}
@color='secondary'
/>
</list.Item>
</Rose::MetadataList>
</div>
-1
View File
@@ -66,7 +66,6 @@
"@playwright/test": "^1.30.0",
"babel-loader": "^8.2.5",
"broccoli-asset-rev": "^3.0.0",
"clipboard": "^2.0.8",
"concurrently": "^8.0.1",
"core": "*",
"cross-env": "^7.0.3",
@@ -280,7 +280,6 @@ module('Acceptance | projects | sessions | index', function (hooks) {
instances.session.update({ status: STATUS_SESSION_TERMINATED });
this.stubClientDaemonSearch('sessions', 'sessions', 'targets');
await visit(urls.sessions);
assert
.dom(`[data-test-session-detail-link="${instances.session.id}"]`)
.doesNotExist();
@@ -106,7 +106,7 @@ module('Acceptance | projects | targets | index', function (hooks) {
type: 'org',
scope: globalScope,
});
const org2Scope = { id: instances.scopes.org2.id, type: 'org' };
const org2Scope = { id: instances.scopes.org.id, type: 'org' };
instances.scopes.project2 = this.server.create('scope', {
type: 'project',
scope: org2Scope,
@@ -1,23 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
module('Integration | Component | copyable', function (hooks) {
setupRenderingTest(hooks);
test('it renders', async function (assert) {
this.set('actionText', 'text');
await render(
hbs`<Copyable @buttonText="Copy me" @text="foo">{{this.actionText}}</Copyable>`,
);
assert.ok(find('.copyable'));
assert.strictEqual(find('.copyable-content').textContent.trim(), 'text');
assert.strictEqual(find('.copyable-button').textContent.trim(), 'Copy me');
});
});
@@ -60,6 +60,6 @@ module('Integration | Component | credentials-panel', function (hooks) {
await click('[data-test-toggle-credentials]');
assert.dom("[data-test-raw-secret]").isVisible();
assert.dom('[data-test-raw-secret]').isVisible();
});
});
@@ -14,6 +14,9 @@ module('Integration | Component | hidden-secret', function (hooks) {
setupIntl(hooks);
setupMirage(hooks);
const HIDDEN_SECRET_TOGGLE = '.hidden-secret > button:first-child';
const HIDDEN_SECRET_CONTENT = '.hidden-secret > .secret-content';
// Global Secret for mocking
this.secret = `{
"secret_key": "QWERTYUIOP",
@@ -37,15 +40,13 @@ module('Integration | Component | hidden-secret', function (hooks) {
test('it renders with a secret', async function (assert) {
assert.expect(1);
await renderComponent();
assert
.dom('.hidden-secret > span > span')
.hasText('■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■');
assert.dom(HIDDEN_SECRET_CONTENT).hasText('■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■');
});
test('it reveals secret when button is clicked', async function (assert) {
assert.expect(1);
await renderComponent();
await click('.hidden-secret > button');
assert.dom('.hidden-secret > span > span').hasText(this.secret);
await click(HIDDEN_SECRET_TOGGLE);
assert.dom(HIDDEN_SECRET_CONTENT).hasText(this.secret);
});
});
-9
View File
@@ -8651,15 +8651,6 @@ clipboard@^2.0.11:
select "^1.1.2"
tiny-emitter "^2.0.0"
clipboard@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.8.tgz#ffc6c103dd2967a83005f3f61976aa4655a4cdba"
integrity sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==
dependencies:
good-listener "^1.2.2"
select "^1.1.2"
tiny-emitter "^2.0.0"
cliui@^7.0.2:
version "7.0.4"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"