ui: CopyableCode component (#13686)

* ui: CopyableCode component plus switch into existing implementations
This commit is contained in:
John Cowen 2022-07-07 17:42:47 +01:00 committed by GitHub
parent 922004d46b
commit 8d275ac186
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 235 additions and 218 deletions

3
.changelog/13686.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:enhancement
ui: Add new CopyableCode component and use it in certain pre-existing areas
```

View File

@ -1,31 +0,0 @@
# Certificate
```hbs preview-template
<Certificate
@item="-----BEGIN CERTIFICATE-----
MIIH/TCCBeWgAwIBAgIQaBYE3/M08XHYCnNVmcFBcjANBgkqhkiG9w0BAQsFADBy
MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24x
ETAPBgNVBAoMCFNTTCBDb3JwMS4wLAYDVQQDDCVTU0wuY29tIEVWIFNTTCBJbnRl
-----END CERTIFICATE-----"
@name="certificate-name"
/>
```
### Arguments
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `item` | `String` | | The certificate as a string to be displayed and copied |
| `name` | `String` | | The 'Name' of the certificate to be displayed and copied. Mainly used for giving feedback to the user. |
This component has the following:
- a copy button
- a visibility button to show and hide certificate
- a hidden and visible display of the certificate
### See
- [Component Source Code](./index.js)
- [Template Source Code](./index.hbs)
---

View File

@ -1,14 +0,0 @@
<div class="certificate">
<CopyButton @value={{@item}} @name={{@name}} />
<button
type="button"
class={{concat "visibility" (if this.show " hide" " show")}}
{{on "click" this.setVisibility}}
>
</button>
{{#if this.show}}
<code>{{@item}}</code>
{{else}}
<hr />
{{/if}}
</div>

View File

@ -1,14 +0,0 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class Certificate extends Component {
// =attributes
@tracked show = false;
// =actions
@action
setVisibility() {
this.show = !this.show;
}
}

View File

@ -1,28 +0,0 @@
.certificate {
display: flex;
button.visibility {
height: fit-content;
padding-top: 4px;
margin-right: 4px;
cursor: pointer;
}
button.hide::before {
@extend %with-visibility-hide-icon, %as-pseudo;
}
button.show::before {
@extend %with-visibility-show-icon, %as-pseudo;
}
code {
background-color: rgb(var(--tone-gray-050));
overflow-wrap: break-word;
max-width: min-content;
padding: 0 12px;
}
hr {
border: 3px dashed rgb(var(--tone-gray-300));
background-color: rgb(var(--tone-gray-000));
width: 150px;
margin: auto;
margin-top: 9px;
}
}

View File

@ -13,23 +13,29 @@
{{#if @item.Config.Host}} {{#if @item.Config.Host}}
<dt>{{t 'models.auth-method.Config.Host'}}</dt> <dt>{{t 'models.auth-method.Config.Host'}}</dt>
<dd> <dd>
<CopyButton @value={{@item.Config.Host}} @name={{t 'models.auth-method.Config.Host'}}/> <CopyableCode
<span>{{@item.Config.Host}}</span> @value={{@item.Config.Host}}
@name={{t 'models.auth-method.Config.Host'}}
/>
</dd> </dd>
{{/if}} {{/if}}
{{#if @item.Config.CACert}} {{#if @item.Config.CACert}}
<dt>{{t 'models.auth-method.Config.CACert'}}</dt> <dt>{{t 'models.auth-method.Config.CACert'}}</dt>
<dd> <dd>
<Certificate @item={{@item.Config.CACert}} @name={{t 'models.auth-method.Config.CACert'}} /> <CopyableCode
@obfuscated={{true}}
@value={{@item.Config.CACert}}
@name={{t 'models.auth-method.Config.CACert'}}
/>
</dd> </dd>
{{/if}} {{/if}}
{{#if @item.Config.ServiceAccountJWT}} {{#if @item.Config.ServiceAccountJWT}}
<dt>{{t 'models.auth-method.Config.ServiceAccountJWT'}}</dt> <dt>{{t 'models.auth-method.Config.ServiceAccountJWT'}}</dt>
<dd> <dd>
<CopyButton @value={{@item.Config.ServiceAccountJWT}} @name={{t 'models.auth-method.Config.ServiceAccountJWT'}} /> <CopyableCode
<span> @value={{@item.Config.ServiceAccountJWT}}
{{@item.Config.ServiceAccountJWT}} @name={{t 'models.auth-method.Config.ServiceAccountJWT'}}
</span> />
</dd> </dd>
{{/if}} {{/if}}
</dl> </dl>
@ -96,25 +102,37 @@ as |item|}}
{{#if @item.Config.JWKSURL}} {{#if @item.Config.JWKSURL}}
<dt>{{t 'models.auth-method.Config.JWKSURL'}}</dt> <dt>{{t 'models.auth-method.Config.JWKSURL'}}</dt>
<dd> <dd>
<CopyButton @value={{@item.Config.JWKSURL}} @name={{t 'models.auth-method.Config.JWKSURL'}} /> <CopyableCode
<span>{{@item.Config.JWKSURL}}</span> @value={{@item.Config.JWKSURL}}
@name={{t 'models.auth-method.Config.JWKSURL'}}
/>
</dd> </dd>
<dt>{{t 'models.auth-method.Config.JWKSCACert'}}</dt> <dt>{{t 'models.auth-method.Config.JWKSCACert'}}</dt>
<dd> <dd>
<Certificate @item={{@item.Config.JWKSCACert}} @name={{t 'models.auth-method.Config.JWKSCACert'}} /> <CopyableCode
@obfuscated={{true}}
@value={{@item.Config.JWKSCACert}}
@name={{t 'models.auth-method.Config.JWKSCACert'}}
/>
</dd> </dd>
{{/if}} {{/if}}
{{#if @item.Config.JWTValidationPubKeys}} {{#if @item.Config.JWTValidationPubKeys}}
<dt>{{t 'models.auth-method.Config.JWTValidationPubKeys'}}</dt> <dt>{{t 'models.auth-method.Config.JWTValidationPubKeys'}}</dt>
<dd> <dd>
<Certificate @item={{@item.Config.JWTValidationPubKeys}} @name={{t 'models.auth-method.Config.JWTValidationPubKeys'}} /> <CopyableCode
@obfuscated={{true}}
@value={{@item.Config.JWTValidationPubKeys}}
@name={{t 'models.auth-method.Config.JWTValidationPubKeys'}}
/>
</dd> </dd>
{{/if}} {{/if}}
{{#if @item.Config.OIDCDiscoveryURL}} {{#if @item.Config.OIDCDiscoveryURL}}
<dt>{{t 'models.auth-method.Config.OIDCDiscoveryURL'}}</dt> <dt>{{t 'models.auth-method.Config.OIDCDiscoveryURL'}}</dt>
<dd> <dd>
<CopyButton @value={{@item.Config.OIDCDiscoveryURL}} @name={{t 'models.auth-method.Config.OIDCDiscoveryURL'}} /> <CopyableCode
<span>{{@item.Config.OIDCDiscoveryURL}}</span> @value={{@item.Config.OIDCDiscoveryURL}}
@name={{t 'models.auth-method.Config.OIDCDiscoveryURL'}}
/>
</dd> </dd>
{{/if}} {{/if}}
{{#if @item.Config.JWTSupportedAlgs}} {{#if @item.Config.JWTSupportedAlgs}}
@ -143,14 +161,20 @@ as |item|}}
{{#if @item.Config.OIDCDiscoveryURL}} {{#if @item.Config.OIDCDiscoveryURL}}
<dt>{{t 'models.auth-method.Config.OIDCDiscoveryURL'}}</dt> <dt>{{t 'models.auth-method.Config.OIDCDiscoveryURL'}}</dt>
<dd> <dd>
<CopyButton @value={{@item.Config.OIDCDiscoveryURL}} @name={{t 'models.auth-method.Config.OIDCDiscoveryURL'}} /> <CopyableCode
<span>{{@item.Config.OIDCDiscoveryURL}}</span> @value={{@item.Config.OIDCDiscoveryURL}}
@name={{t 'models.auth-method.Config.OIDCDiscoveryURL'}}
/>
</dd> </dd>
{{/if}} {{/if}}
{{#if @item.Config.OIDCDiscoveryCACert}} {{#if @item.Config.OIDCDiscoveryCACert}}
<dt>{{t 'models.auth-method.Config.OIDCDiscoveryCACert'}}</dt> <dt>{{t 'models.auth-method.Config.OIDCDiscoveryCACert'}}</dt>
<dd> <dd>
<Certificate @item={{@item.Config.OIDCDiscoveryCACert}} @name={{t 'models.auth-method.Config.OIDCDiscoveryCACert'}} /> <CopyableCode
@obfuscated={{true}}
@value={{@item.Config.OIDCDiscoveryCACert}}
@name={{t 'models.auth-method.Config.OIDCDiscoveryCACert'}}
/>
</dd> </dd>
{{/if}} {{/if}}
{{#if @item.Config.OIDCClientID}} {{#if @item.Config.OIDCClientID}}
@ -167,8 +191,10 @@ as |item|}}
<ul> <ul>
{{#each @item.Config.AllowedRedirectURIs as |uri|}} {{#each @item.Config.AllowedRedirectURIs as |uri|}}
<li> <li>
<CopyButton @value={{uri}} @name="Redirect URI" /> <CopyableCode
<span>{{uri}}</span> @value={{uri}}
@name="Redirect URI"
/>
</li> </li>
{{/each}} {{/each}}
</ul> </ul>

View File

@ -0,0 +1,50 @@
# CopyableCode
CopyableCode is used to display code that is likely to be copied by the user.
It also has an option to obfuscate the code, that is then toggleable by the user.
Text within the component is formatted using a `<pre>` tag.
```hbs preview-template
<figure>
<figcaption>Without obfuscation</figcaption>
<CopyableCode
@value="-----BEGIN CERTIFICATE-----
MIIH/TCCBeWgAwIBAgIQaBYE3/M08XHYCnNVmcFBcjANBgkqhkiG9w0BAQsFADBy
MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24x
ETAPBgNVBAoMCFNTTCBDb3JwMS4wLAYDVQQDDCVTU0wuY29tIEVWIFNTTCBJbnRl
-----END CERTIFICATE-----"
@name="Name of thing that gets copied e.g. Certificate"
/>
</figure>
```
```hbs preview-template
<figure>
<figcaption>With obfuscation</figcaption>
<CopyableCode
@obfuscated={{true}}
@value="-----BEGIN CERTIFICATE-----
MIIH/TCCBeWgAwIBAgIQaBYE3/M08XHYCnNVmcFBcjANBgkqhkiG9w0BAQsFADBy
MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0b24x
ETAPBgNVBAoMCFNTTCBDb3JwMS4wLAYDVQQDDCVTU0wuY29tIEVWIFNTTCBJbnRl
-----END CERTIFICATE-----"
@name="Certificate"
/>
</figure>
```
### Arguments
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `String` | | The code to display/be copied |
| `name` | `String` | | The 'Name' of the thing to be displayed and copied. Mainly used for giving feedback to the user. |
| `obfuscated` | `boolean` | | Whether to obfuscate the value until the user clicks to view |
### See
- [Template Source Code](./index.hbs)
---

View File

@ -0,0 +1,45 @@
<div
class={{class-map
'copyable-code'
(array 'obfuscated' @obfuscated)
}}
...attributes
>
{{#if @obfuscated}}
<Disclosure
as |disclosure|>
<disclosure.Action
{{on 'click' disclosure.toggle}}
aria-label={{if details.expanded 'Hide' 'Show'}}
>
</disclosure.Action>
<disclosure.Details as |details|>
<pre><code
id={{details.id}}
>{{@value}}</code></pre>
</disclosure.Details>
<disclosure.Details
@auto={{false}}
as |details|>
{{#if (not details.expanded)}}
<hr />
{{/if}}
</disclosure.Details>
</Disclosure>
<CopyButton
@value={{@value}}
@name={{@name}}
/>
{{else}}
<pre><code
>{{@value}}</code></pre>
<CopyButton
@value={{@value}}
@name={{@name}}
/>
{{/if}}
</div>

View File

@ -0,0 +1,65 @@
.copyable-code {
& {
display: flex;
align-items: flex-start;
position: relative;
width: 100%;
padding: 8px 14px;
padding-bottom: 3px;
border: var(--decor-border-100);
border-color: rgb(var(--tone-gray-200));
border-radius: var(--decor-radius-200);
}
&.obfuscated {
padding-left: 4px;
}
&::after {
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 100%;
display: block;
content: '';
background-color: rgb(var(--tone-gray-050));
}
.copy-button {
position: absolute;
top: 7px;
right: 12px;
}
button[aria-expanded] {
margin-top: 1px;
margin-right: 4px;
cursor: pointer;
}
button[aria-expanded]::before {
content: '';
--icon-size: icon-000;
--icon-color: rgb(var(--tone-gray-500));
}
button[aria-expanded=true]::before {
--icon-name: icon-eye-off;
}
button[aria-expanded=false]::before {
--icon-name: icon-eye;
}
pre {
padding-right: 30px;
}
code {
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
hr {
width: calc(100% - 80px);
margin: 0;
margin-top: 8px;
margin-bottom: 13px;
border: 3px dashed rgb(var(--tone-gray-300));
background-color: rgb(var(--tone-gray-000));
}
}

View File

@ -4,7 +4,6 @@ class: css
# definition-table # definition-table
Simple CSS component to render a `dl` similar to a table with column headers. Simple CSS component to render a `dl` similar to a table with column headers.
Contents of the `dd` are currently inline-block'ed for CopyButton reasons.
```hbs preview-template ```hbs preview-template
<div class="definition-table"> <div class="definition-table">
@ -12,7 +11,12 @@ Contents of the `dd` are currently inline-block'ed for CopyButton reasons.
<dt>Title 1</dt> <dt>Title 1</dt>
<dd>Value</dd> <dd>Value</dd>
<dt>Title 2</dt> <dt>Title 2</dt>
<dd><CopyButton @name="Title 2" @value="Value"/>Value</dd> <dd>
<CopyableCode
@name="Title 2"
@value="Value"
/>
</dd>
</dl> </dl>
</div> </div>
``` ```

View File

@ -6,10 +6,3 @@
%definition-table > dl { %definition-table > dl {
margin-bottom: 1.4em; margin-bottom: 1.4em;
} }
/* TODO: We currently have one instance of nested dls */
/* and that is for nesting a bucket list */
/* we should probably think about changing this to possibly inline flex */
/* or individually styling the contents */
%definition-table > dl > dd > *:not(dl) {
display: inline-block;
}

View File

@ -1,5 +0,0 @@
<label class="type-reveal">
<input type="checkbox" />
<span>Reveal</span>
<em>{{yield}}</em>
</label>

View File

@ -1,3 +0,0 @@
import Component from '@ember/component';
export default Component.extend({});

View File

@ -1,23 +0,0 @@
@import './skin';
@import './layout';
.type-reveal {
@extend %secret-button;
}
%secret-button {
position: relative;
}
%secret-button span {
visibility: hidden;
position: absolute;
--icon-color: rgb(var(--tone-gray-500));
}
%secret-button em {
margin-left: 22px;
}
%secret-button span::before {
@extend %with-visibility-show-mask, %as-pseudo;
visibility: visible;
}
%secret-button input:checked + span::before {
@extend %with-visibility-hide-mask;
}

View File

@ -1,23 +0,0 @@
%secret-button {
cursor: pointer;
}
%secret-button input {
display: none;
}
%secret-button input ~ em {
visibility: hidden;
font-style: normal;
}
%secret-button input:checked ~ em {
@extend %user-select-text;
visibility: visible;
cursor: auto;
}
%secret-button input ~ em::before {
display: inline;
visibility: visible;
content: '■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■';
}
%secret-button input:checked ~ em::before {
display: none;
}

View File

@ -25,10 +25,6 @@
dd > ul li:not(:last-of-type) { dd > ul li:not(:last-of-type) {
padding-bottom: 12px; padding-bottom: 12px;
} }
dd .copy-button button {
padding: 0 !important;
margin: 0 4px 0 0 !important;
}
dt.check + dd { dt.check + dd {
padding-top: 16px; padding-top: 16px;
} }

View File

@ -154,8 +154,8 @@
// @import './user-square-fill/index.scss'; // @import './user-square-fill/index.scss';
// @import './user-square-outline/index.scss'; // @import './user-square-outline/index.scss';
@import './user-team/index.scss'; @import './user-team/index.scss';
@import './visibility-hide/index.scss'; // @import './visibility-hide/index.scss';
@import './visibility-show/index.scss'; // @import './visibility-show/index.scss';
// @import './webhook/index.scss'; // @import './webhook/index.scss';
// @import './activity/index.scss'; // @import './activity/index.scss';
@import './alert-circle/index.scss'; @import './alert-circle/index.scss';
@ -316,8 +316,8 @@
// @import './event/index.scss'; // @import './event/index.scss';
// @import './exit-point/index.scss'; // @import './exit-point/index.scss';
// @import './external-link/index.scss'; // @import './external-link/index.scss';
// @import './eye/index.scss'; @import './eye/index.scss';
// @import './eye-off/index.scss'; @import './eye-off/index.scss';
// @import './f5/index.scss'; // @import './f5/index.scss';
// @import './f5-color/index.scss'; // @import './f5-color/index.scss';
// @import './facebook/index.scss'; // @import './facebook/index.scss';

View File

@ -33,7 +33,6 @@
@import 'consul-ui/components/popover-select'; @import 'consul-ui/components/popover-select';
@import 'consul-ui/components/progress'; @import 'consul-ui/components/progress';
@import 'consul-ui/components/radio-group'; @import 'consul-ui/components/radio-group';
@import 'consul-ui/components/secret-button';
@import 'consul-ui/components/sliding-toggle'; @import 'consul-ui/components/sliding-toggle';
@import 'consul-ui/components/table'; @import 'consul-ui/components/table';
@import 'consul-ui/components/tile'; @import 'consul-ui/components/tile';
@ -75,7 +74,7 @@
@import 'consul-ui/components/informed-action'; @import 'consul-ui/components/informed-action';
@import 'consul-ui/components/tab-nav'; @import 'consul-ui/components/tab-nav';
@import 'consul-ui/components/search-bar'; @import 'consul-ui/components/search-bar';
@import 'consul-ui/components/certificate'; @import 'consul-ui/components/copyable-code';
@import 'consul-ui/components/consul/loader'; @import 'consul-ui/components/consul/loader';
@import 'consul-ui/components/consul/tomography/graph'; @import 'consul-ui/components/consul/tomography/graph';

View File

@ -252,9 +252,6 @@ html.with-route-announcer .route-title {
figcaption code { figcaption code {
@extend %inline-code; @extend %inline-code;
} }
pre code {
@extend %block-code;
}
figure > [type='text'] { figure > [type='text'] {
border: 1px solid rgb(var(--tone-gray-999)); border: 1px solid rgb(var(--tone-gray-999));
width: 100%; width: 100%;

View File

@ -62,7 +62,10 @@ as |dc partition nspace id item create|}}
<dl> <dl>
<dt>Policy ID</dt> <dt>Policy ID</dt>
<dd> <dd>
<CopyButton @value={{item.ID}} @name="Policy ID" @position="top-start" /> {{item.ID}} <CopyableCode
@value={{item.ID}}
@name="Policy ID"
/>
</dd> </dd>
</dl> </dl>
</div> </div>

View File

@ -56,7 +56,10 @@ as |dc partition nspace item create|}}
<dl> <dl>
<dt>Role ID</dt> <dt>Role ID</dt>
<dd> <dd>
<CopyButton @value={{item.ID}} @name="Role ID" @position="top-start" /> {{item.ID}} <CopyableCode
@value={{item.ID}}
@name="Role ID"
/>
</dd> </dd>
</dl> </dl>
</div> </div>

View File

@ -91,11 +91,18 @@ as |dc partition nspace item create|}}
<dl> <dl>
<dt>AccessorID</dt> <dt>AccessorID</dt>
<dd> <dd>
<CopyButton @value={{item.AccessorID}} @name="AccessorID" @position="top-start" /> {{item.AccessorID}} <CopyableCode
@value={{item.AccessorID}}
@name="AccessorID"
/>
</dd> </dd>
<dt>Token</dt> <dt>Token</dt>
<dd> <dd>
<CopyButton @value={{item.SecretID}} @name="Token" @position="top-start" /> <SecretButton>{{item.SecretID}}</SecretButton> <CopyableCode
@obfuscated={{true}}
@value={{item.SecretID}}
@name="Token"
/>
</dd> </dd>
{{#if (and (not (token/is-legacy item)) (not create))}} {{#if (and (not (token/is-legacy item)) (not create))}}
<dt>Scope</dt> <dt>Scope</dt>

View File

@ -1,33 +0,0 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render, find } from '@ember/test-helpers';
import hbs from 'htmlbars-inline-precompile';
module('Integration | Component | secret button', function(hooks) {
setupRenderingTest(hooks);
test('it renders', async function(assert) {
// Set any properties with this.set('myProperty', 'value');
// Handle any actions with this.on('myAction', function(val) { ... });
await render(hbs`{{secret-button}}`);
assert.ok(
find('*')
.textContent.trim()
.indexOf('Reveal') !== -1
);
// Template block usage:
await render(hbs`
{{#secret-button}}
{{/secret-button}}
`);
assert.ok(
find('*')
.textContent.trim()
.indexOf('Reveal') !== -1
);
});
});