[ui] Multi-line variable values and helios upgrades generally (#19544)

* Multi-line variable values and helios upgrades generally

* Variables page titles and actions restyle

* Hacky fix to keyboard shortcut otherwise bumping space on shift

* Related entities heliosified

* Namespace and path fields heliosed

* Paths table heliosified

* Variable view table

* Fixups after design discussion

* Monospaced editing

* De-commented template placeholder

* Acceptance tests updated for helios components across variables

* Tests helios'd in variable-form-test

* PR suggestions
This commit is contained in:
Phil Renaud
2024-01-03 15:54:22 -05:00
committed by GitHub
parent d75293d2ab
commit 89cceebb91
16 changed files with 441 additions and 435 deletions

View File

@@ -50,22 +50,21 @@
{{/if}}
<div class={{if this.namespaceOptions "path-namespace"}}>
<label>
<span>
Path
</span>
<Input
@type="text"
@value={{this.path}}
placeholder="nomad/jobs/my-job/my-group/my-task"
class="input path-input {{if this.duplicatePathWarning "error"}}"
{{on "input" this.setModelPath}}
disabled={{not @model.isNew}}
{{autofocus}}
data-test-path-input
/>
<Hds::Form::TextInput::Field
@isRequired={{true}}
@value={{this.path}}
placeholder="nomad/jobs/my-job/my-group/my-task"
@isInvalid={{this.duplicatePathWarning}}
{{on "input" (action this.setModelPath)}}
disabled={{not @model.isNew}}
{{autofocus}}
data-test-path-input
as |F|>
<F.Label>Path</F.Label>
{{#if this.duplicatePathWarning}}
<p class="duplicate-path-error help is-danger">
<F.Error data-test-duplicate-variable-error>
There is already a variable located at
{{this.path}}
.
@@ -78,19 +77,16 @@
edit the existing variable
</LinkTo>
.
</p>
</F.Error>
{{/if}}
{{#if this.isJobTemplateVariable}}
<p class="help job-template-hint">
Use this variable to generate job templates with
<code>nomad job init -template={{this.jobTemplateName}}
<CopyButton
@clipboardText="nomad job init -template={{this.jobTemplateName}}"
/>
</code>
</p>
<F.HelperText data-test-job-template-hint>
Use this variable to generate job templates with <Hds::Copy::Snippet @textToCopy="nomad job init -template={{this.jobTemplateName}}" />
</F.HelperText>
{{/if}}
</label>
</Hds::Form::TextInput::Field>
<VariableForm::NamespaceFilter
@data={{hash
disabled=(not @model.isNew)
@@ -131,28 +127,20 @@
{{else}}
{{#each this.keyValues as |entry iter|}}
<div class="key-value">
<label>
<span>
Key
</span>
<Input
data-test-var-key
@type="text"
@value={{entry.key}}
class="input"
{{autofocus ignore=(eq iter 0)}}
{{on "input" (fn this.validateKey entry)}}
/>
</label>
<Hds::Form::TextInput::Field
@value={{entry.key}}
data-test-var-key
class="input"
{{autofocus ignore=(eq iter 0)}}
{{on "input" (action this.validateKey entry)}}
as |F|>
<F.Label>Key</F.Label>
</Hds::Form::TextInput::Field>
<VariableForm::InputGroup @entry={{entry}} />
<button
class="delete-row button is-danger is-inverted"
type="button"
{{on "click" (action this.deleteRow entry)}}
<Hds::Button @text="Delete" @color="critical" class="delete-entry-button"
disabled={{eq this.keyValues.length 1}}
>
Delete
</button>
{{on "click" (action this.deleteRow entry)}}
/>
{{#each-in entry.warnings as |k v|}}
<span class="key-value-error help is-danger">
{{v}}
@@ -166,25 +154,23 @@
<footer>
{{#unless this.isJSONView}}
{{#unless this.isJobTemplateVariable}}
<button
class="add-more button is-info is-inverted"
type="button"
disabled={{not (and this.keyValues.lastObject.key this.keyValues.lastObject.value)}}
{{on "click" this.appendRow}}
<Hds::Button
@text="Add More"
@color="secondary"
@size="medium"
@icon="plus"
{{!-- only enable if the last entry isn't empty --}}
disabled={{not (and this.keyValues.lastObject.key this.keyValues.lastObject.value)}} {{on "click" this.appendRow}}
data-test-add-kv
>
Add More
</button>
/>
{{/unless}}
{{/unless}}
<button
disabled={{this.shouldDisableSave}}
class="button is-primary save"
<Hds::Button
@text="Save {{pluralize "Variable" this.keyValues.length}}"
@color="primary"
type="submit"
disabled={{this.shouldDisableSave}}
data-test-submit-var
>
Save
{{pluralize "Variable" @this.keyValues.length}}
</button>
/>
</footer>
</form>

View File

@@ -180,6 +180,7 @@ export default class VariableFormComponent extends Component {
delete entry.warnings.duplicateKeyError;
entry.warnings.notifyPropertyChange('duplicateKeyError');
}
set(entry, 'key', value);
}
@action appendRow() {
@@ -207,6 +208,7 @@ export default class VariableFormComponent extends Component {
* @param {KeyboardEvent} e
*/
@action setModelPath(e) {
set(this, 'path', e.target.value);
set(this.args.model, 'path', e.target.value);
}

View File

@@ -4,26 +4,15 @@
~}}
<label class="value-label">
<span>
Value
</span>
<Input
@type={{this.inputType}}
@value={{@entry.value}}
class="input value-input"
{{! prevent auto-fill }}
autocomplete="new-password"
data-test-var-value
/>
<button
class="show-hide-values button is-light"
type="button"
tabindex="-1"
{{on "click" this.toggleInputType}}
>
<FlightIcon
@name={{if this.isObscured "eye-off" "eye"}}
@title={{if this.isObscured "Show Value" "Hide Value"}}
/>
</button>
<Hds::Form::MaskedInput::Field
@isMultiline={{true}}
@value={{@entry.value}}
rows="1"
class="hds-typography-code-200"
{{! prevent auto-fill }}
autocomplete="new-password"
data-test-var-value
as |F|>
<F.Label>Value</F.Label>
</Hds::Form::MaskedInput::Field>
</label>

View File

@@ -10,19 +10,18 @@
{{#if trigger.data.isSuccess}}
{{#if trigger.data.result}}
{{#if @data.namespaceOptions}}
<label>
<span>
Namespace
</span>
<SingleSelectDropdown
data-test-variable-namespace-filter
@label="Namespace"
@disabled={{@data.disabled}}
@options={{@data.namespaceOptions}}
@selection={{@data.selection}}
@onSelect={{@fns.onSelect}}
/>
</label>
<Hds::Dropdown class="namespace-dropdown" data-test-variable-namespace-filter as |dd|>
<dd.ToggleButton @text="{{@data.selection}}" @color="secondary" disabled={{@data.disabled}} @isFullWidth={{true}} />
{{#each @data.namespaceOptions as |option|}}
<dd.Radio
name={{option.key}}
{{on "change" (action @fns.onSelect option.key)}}
checked={{eq @data.selection option.key}}
>
{{option.label}}
</dd.Radio>
{{/each}}
</Hds::Dropdown>
{{/if}}
{{/if}}
{{/if}}

View File

@@ -3,18 +3,18 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<p class="related-entities notification">
<FlightIcon @name="info-fill" @color="var(--blue)" />
<span>
<Hds::Alert @type="inline" @color="highlight" @icon="info" class="related-entities notification" as |A|>
<A.Title>Automatically-accessible variable</A.Title>
<A.Description>
This variable {{#if @new}}will be{{else}}is{{/if}} accessible by
{{#if @task}}
task <strong>{{@task}}</strong> in group <LinkTo @route="jobs.job.task-group" @models={{array (concat @job "@" @namespace) @group}}>{{@group}} <FlightIcon @name="external-link" /></LinkTo>
task <strong>{{@task}}</strong> in group <Hds::Link::Inline @route="jobs.job.task-group" @models={{array (concat @job "@" @namespace) @group}} @icon="external-link">{{@group}}</Hds::Link::Inline>
{{else if @group}}
group <LinkTo @route="jobs.job.task-group" @models={{array (concat @job "@" @namespace) @group}}>{{@group}} <FlightIcon @name="external-link" /></LinkTo>
group <Hds::Link::Inline @route="jobs.job.task-group" @models={{array (concat @job "@" @namespace) @group}} @icon="external-link">{{@group}}</Hds::Link::Inline>
{{else if @job}}
job <LinkTo @route="jobs.job" @model={{concat @job "@" @namespace}}>{{@job}} <FlightIcon @name="external-link" /></LinkTo>
job <Hds::Link::Inline @route="jobs.job" @model={{concat @job "@" @namespace}} @icon="external-link">{{@job}}</Hds::Link::Inline>
{{else}}
all nomad jobs in this namespace
{{/if}}
</span>
</p>
all nomad jobs in this namespace
{{/if}}
</A.Description>
</Hds::Alert>

View File

@@ -3,22 +3,24 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<ListTable class="path-tree" @source={{@branch}} as |t|>
<t.head>
<th>
Path
</th>
<th>
Namespace
</th>
<th>
Last Modified
</th>
</t.head>
<tbody>
<Hds::Table @caption="A list variables" class="path-tree">
<:head as |H|>
<H.Tr>
<H.Th>
Path
</H.Th>
<H.Th>
Namespace
</H.Th>
<H.Th>
Last Modified
</H.Th>
</H.Tr>
</:head>
<:body as |B|>
{{#each this.folders as |folder|}}
<tr data-test-folder-row {{on "click" (fn this.handleFolderClick folder.data.absolutePath)}}>
<td colspan="3"
<B.Tr data-test-folder-row {{on "click" (fn this.handleFolderClick folder.data.absolutePath)}}>
<B.Td colspan="3"
{{keyboard-shortcut
enumerated=true
action=(fn this.handleFolderClick folder.data.absolutePath)
@@ -30,12 +32,13 @@
{{trim-path folder.name}}
</LinkTo>
</span>
</td>
</tr>
</B.Td>
</B.Tr>
{{/each}}
{{#each this.files as |file|}}
<tr
<B.Tr
data-test-file-row="{{file.name}}"
{{on "click" (fn this.handleFileClick file)}}
class={{if (can "read variable" path=file.absoluteFilePath namespace=file.variable.namespace) "" "inaccessible"}}
@@ -44,7 +47,7 @@
action=(fn this.handleFileClick file)
}}
>
<td>
<B.Td>
<FlightIcon @name="file-text" />
{{#if (can "read variable" path=file.absoluteFilePath namespace=file.variable.namespace)}}
<LinkTo
@@ -57,17 +60,16 @@
{{else}}
<span title="Your access policy does not allow you to view the contents of {{file.name}}">{{file.name}}</span>
{{/if}}
</td>
<td>
</B.Td>
<B.Td>
{{file.variable.namespace}}
</td>
<td>
</B.Td>
<B.Td>
<span class="tooltip" aria-label="{{format-ts file.variable.modifyTime}}">
{{moment-from-now file.variable.modifyTime}}
</span>
</td>
</tr>
{{/each}}
</tbody>
</ListTable>
</B.Td>
</B.Tr>
{{/each}}
</:body>
</Hds::Table>

View File

@@ -37,6 +37,10 @@ export default class VariablesVariableIndexController extends Controller {
this.isDeleting = false;
}
@action copyVariable() {
navigator.clipboard.writeText(JSON.stringify(this.model.items, null, 2));
}
@task(function* () {
try {
yield this.model.deleteRecord();

View File

@@ -5,28 +5,18 @@
.section.single-variable {
margin-top: 1.5rem;
.back-link {
text-decoration: none;
color: #363636;
position: relative;
top: 4px;
}
}
$hdsLabelTopOffset: 26px;
$hdsInputHeight: 35px;
.variable-title {
.toggle {
font-size: 0.8rem;
margin-left: 1rem;
position: relative;
top: -0.25rem;
.toggler {
margin-right: 0.25rem;
}
margin-bottom: 2rem;
.hds-page-header__main {
flex-direction: unset;
}
.copy-button {
position: relative;
top: 3px;
.copy-variable span {
color: var(--token-color-foreground-primary);
}
}
@@ -35,18 +25,6 @@
margin-bottom: 1rem;
}
.path-input {
height: 2.25em;
&:disabled {
background-color: #f5f5f5;
}
&.error {
color: $red;
border-color: $red;
}
}
.duplicate-path-error {
position: relative;
animation: slide-in 0.3s ease-out;
@@ -56,13 +34,21 @@
display: grid;
grid-template-columns: 6fr 1fr;
gap: 0 1rem;
align-items: start;
.namespace-dropdown {
white-space: nowrap;
width: auto;
position: relative;
top: $hdsLabelTopOffset;
height: $hdsInputHeight;
}
}
.key-value {
display: grid;
grid-template-columns: 1fr 4fr 130px;
gap: 0 1rem;
align-items: end;
align-items: start;
input.error {
color: $red;
@@ -77,6 +63,12 @@
}
}
.delete-entry-button {
position: relative;
top: $hdsLabelTopOffset;
height: $hdsInputHeight;
}
button.show-hide-values {
height: 100%;
box-shadow: none;
@@ -131,11 +123,6 @@
grid-auto-columns: max-content;
grid-auto-flow: column;
gap: 1rem;
.button.is-info.is-inverted.add-more[disabled] {
border-color: #dbdbdb;
box-shadow: 0 2px 0 0 rgb(122 122 122 / 20%);
}
}
}
@@ -152,20 +139,8 @@ table.path-tree {
}
}
.section .notification.related-entities {
--blue: #1563ff;
display: flex;
align-items: center;
gap: 0.5rem;
&.notification {
align-items: center;
}
a {
color: $blue;
display: inline-flex;
align-items: center;
gap: 0.25rem;
}
.related-entities {
margin-bottom: 2rem;
}
.related-entities-hint {
@@ -178,25 +153,6 @@ table.path-tree {
}
}
.job-template-hint {
margin-top: 0.5rem;
code {
background-color: #eee;
padding: 0.25rem;
}
.copy-button {
display: inline-block;
padding-left: 0;
position: relative;
top: -5px;
button,
.button {
background-color: transparent;
padding-right: 0.25rem;
}
}
}
table.variable-items {
// table-layout: fixed;
td.value-cell {

View File

@@ -5,46 +5,50 @@
{{page-title "Variables"}}
<section class="section">
<div class="toolbar">
<div class="toolbar-item is-right-aligned is-mobile-full-width">
<Hds::PageHeader class="variable-title" as |PH|>
<PH.Actions>
{{#if this.namespaceOptions}}
<SingleSelectDropdown
data-test-variable-namespace-filter
@label="Namespace"
@options={{this.namespaceOptions}}
@selection={{this.namespaceSelection}}
@onSelect={{this.setNamespace}}
<Hds::Dropdown data-test-variable-namespace-filter as |dd|>
<dd.ToggleButton @text="Namespace ({{this.namespaceSelection}})" @color="secondary" />
{{#each this.namespaceOptions as |option|}}
<dd.Radio
name={{option.key}}
{{on "change" (action this.setNamespace option.key)}}
checked={{eq this.namespaceSelection option.key}}
>
{{option.label}}
</dd.Radio>
{{/each}}
</Hds::Dropdown>
{{/if}}
{{#if (can "write variable" path="*" namespace=this.namespaceSelection)}}
<div
{{keyboard-shortcut
pattern=(array "n" "v")
action=(action this.goToNewVariable)
label="Create Variable"
}}
>
<Hds::Button
@text="Create Variable"
@icon="plus"
@route="variables.new"
data-test-create-var
/>
</div>
{{else}}
<Hds::Button
@text="Create Variable"
@icon="plus"
data-test-disabled-create-var
disabled
/>
{{/if}}
<div class="button-bar">
{{#if (can "write variable" path="*" namespace=this.namespaceSelection)}}
<LinkTo
@route="variables.new"
class="button is-primary"
data-test-create-var
{{keyboard-shortcut
pattern=(array "n" "v")
action=(action this.goToNewVariable)
label="Create Variable"
}}
>
Create Variable
</LinkTo>
{{else}}
<button
class="button is-primary is-disabled tooltip is-right-aligned"
aria-label="You dont have sufficient permissions"
disabled
type="button"
data-test-disabled-create-var
>
Create Variable
</button>
{{/if}}
</PH.Actions>
</Hds::PageHeader>
</div>
</div>
</div>
{{#if this.isForbidden}}
<ForbiddenMessage />
{{else}}

View File

@@ -7,15 +7,24 @@
<Breadcrumb @crumb={{hash label="New" args=(array "variables.new")}} />
<section class="section">
<h1 class="title variable-title">
Create a Variable
<Toggle
data-test-memory-toggle
@isActive={{eq this.view "json"}}
@onToggle={{action this.toggleView}}
title="JSON"
>JSON</Toggle>
</h1>
<Hds::PageHeader class="variable-title" as |PH|>
<PH.Title>Create a Variable</PH.Title>
<PH.Actions>
<Hds::Form::Toggle::Field
@value="enable"
{{keyboard-shortcut
label="Toggle View (JSON/List)"
pattern=(array "j")
action=(action this.toggleView)
}}
checked={{eq this.view "json"}}
data-test-json-toggle
{{on "change" (action this.toggleView)}}
as |F|>
<F.Label>JSON</F.Label>
</Hds::Form::Toggle::Field>
</PH.Actions>
</Hds::PageHeader>
<VariableForm
@model={{this.model}}

View File

@@ -8,44 +8,50 @@
<Breadcrumb @crumb={{crumb}} />
{{/each}}
<section class="section">
<div class="toolbar">
<div class="toolbar-item is-right-aligned is-mobile-full-width">
{{#if this.namespaceOptions}}
<SingleSelectDropdown
data-test-variable-namespace-filter
@label="Namespace"
@options={{this.namespaceOptions}}
@selection={{this.namespaceSelection}}
@onSelect={{this.setNamespace}}
/>
{{/if}}
<div class="button-bar">
<Hds::PageHeader class="variable-title" as |PH|>
<PH.Title>/{{this.absolutePath}}</PH.Title>
<PH.Actions>
{{#if this.namespaceOptions}}
<Hds::Dropdown data-test-variable-namespace-filter as |dd|>
<dd.ToggleButton @text="Namespace ({{this.namespaceSelection}})" @color="secondary" />
{{#each this.namespaceOptions as |option|}}
<dd.Radio
name={{option.key}}
{{on "change" (action this.setNamespace option.key)}}
checked={{eq this.namespaceSelection option.key}}
>
{{option.label}}
</dd.Radio>
{{/each}}
</Hds::Dropdown>
{{/if}}
{{#if (can "write variable" path=(concat this.absolutePath "/") namespace=this.namespaceSelection)}}
<LinkTo
@route="variables.new"
@query={{hash path=(concat this.absolutePath "/")}}
class="button is-primary"
<div
{{keyboard-shortcut
pattern=(array "n" "v")
action=(action this.goToNewVariable)
label="Create Variable"
}}
>
Create Variable
</LinkTo>
<Hds::Button
@text="Create Variable"
@icon="plus"
@route="variables.new"
@query={{hash path=(concat this.absolutePath "/")}}
data-test-create-var
/>
</div>
{{else}}
<button
class="button is-primary is-disabled tooltip is-right-aligned"
aria-label="You dont have sufficient permissions"
<Hds::Button
@text="Create Variable"
@icon="plus"
data-test-create-var
disabled
type="button"
>
Create Variable
</button>
/>
{{/if}}
</div>
</div>
</div>
</PH.Actions>
</Hds::PageHeader>
{{#if this.isForbidden}}
<ForbiddenMessage />
{{else}}

View File

@@ -4,25 +4,30 @@
~}}
{{page-title "Edit Variable"}}
<h1 class="title variable-title">
<LinkTo class="back-link" @route="variables.variable.index">
<FlightIcon
@name="chevron-left"
@title="Back to {{this.model.path}}"
@size="24"
/>
</LinkTo>
Edit
{{this.model.path}}
<Toggle
data-test-json-toggle
@isActive={{eq this.view "json"}}
@onToggle={{action this.toggleView}}
title="JSON"
>JSON</Toggle>
</h1>
<Hds::PageHeader class="variable-title" as |PH|>
<PH.Title>Editing {{this.model.path}}</PH.Title>
<PH.IconTile @icon="file-text" />
<PH.Actions>
<Hds::Form::Toggle::Field
@value="enable"
{{keyboard-shortcut
label="Toggle View (JSON/List)"
pattern=(array "j")
action=(action this.toggleView)
}}
checked={{eq this.view "json"}}
data-test-json-toggle
{{on "change" (action this.toggleView)}}
as |F|>
<F.Label>JSON</F.Label>
</Hds::Form::Toggle::Field>
</PH.Actions>
<PH.Breadcrumb>
<Hds::Breadcrumb>
<Hds::Breadcrumb::Item @text="Back" @route="variables.variable.index" @icon="chevron-left" />
</Hds::Breadcrumb>
</PH.Breadcrumb>
</Hds::PageHeader>
<VariableForm
@model={{this.model}}

View File

@@ -3,45 +3,54 @@
SPDX-License-Identifier: BUSL-1.1
~}}
<h1 class="variable-title title with-flex">
<div>
<FlightIcon @name="file-text" />
{{this.model.path}}
<CopyButton
@inset={{true}}
@compact={{true}}
@clipboardText={{this.model.path}}
/>
<Hds::PageHeader class="variable-title" as |PH|>
<PH.Title>{{this.model.path}}</PH.Title>
<PH.IconTile @icon="file-text" />
<PH.Actions>
{{#unless this.isDeleting}}
<Toggle
<Hds::Form::Toggle::Field
@value="enable"
{{keyboard-shortcut
label="Toggle View (JSON/List)"
pattern=(array "j")
action=(action this.toggleView)
}}
data-test-memory-toggle
@isActive={{eq this.view "json"}}
@onToggle={{action this.toggleView}}
title="JSON"
>JSON</Toggle>
</div>
<div>
{{#unless this.isDeleting}}
checked={{eq this.view "json"}}
data-test-json-toggle
{{on "change" (action this.toggleView)}}
as |F|>
<F.Label>JSON</F.Label>
</Hds::Form::Toggle::Field>
<div
{{keyboard-shortcut
label="Copy Variable"
pattern=(array "c" "v")
action=(action this.copyVariable)}}
>
<Hds::Copy::Button
@text="Copy"
@textToCopy={{stringify-object this.model.items}}
@isIconOnly={{true}}
class="copy-variable"
/>
</div>
{{#if (can "write variable" path=this.model.path namespace=this.model.namespace)}}
<div class="two-step-button">
<LinkTo
{{autofocus}}
data-test-edit-button
class="button is-info is-inverted is-small"
@model={{this.model}}
<Hds::Button
@icon="edit"
@text="Edit"
@color="secondary"
@route="variables.variable.edit"
@model={{this.model}}
@query={{hash view=this.view}}
>
Edit
</LinkTo>
</div>
data-test-edit-button
{{autofocus}}
/>
{{/if}}
{{/unless}}
{{#if (can "destroy variable" path=this.model.path namespace=this.model.namespace)}}
<TwoStepButton
data-test-delete-button
@@ -56,8 +65,8 @@
@onCancel={{this.onDeleteCancel}}
/>
{{/if}}
</div>
</h1>
</PH.Actions>
</Hds::PageHeader>
{{#if this.shouldShowLinkedEntities}}
<VariableForm::RelatedEntities
@@ -72,59 +81,65 @@
<div class="boxed-section">
<div class="boxed-section-head">
Key/Value Data
<CopyButton
class="pull-right"
@compact={{true}}
@border={{true}}
@clipboardText={{stringify-object this.model.items}}
/>
</div>
<div class="boxed-section-body is-full-bleed">
<JsonViewer @json={{this.model.items}} />
</div>
</div>
{{else}}
<ListTable class="variable-items" @source={{this.sortedKeyValues}} @sortProperty={{this.sortProperty}} @sortDescending={{this.sortDescending}} as |t|>
<t.head>
<t.sort-by @prop="key">Key</t.sort-by>
<t.sort-by @prop="value">Value</t.sort-by>
</t.head>
<t.body as |row|>
<tr data-test-var={{row.model.key}}>
<td>
{{row.model.key}}
</td>
<td colspan="3" class="value-cell">
<Hds::Table class="variable-items"
@model={{this.sortedKeyValues}}
@sortBy={{this.sortProperty}}
@sortOrder={{if this.sortDescending "desc" "asc"}}
@columns={{array
(hash
key="key"
label="Key"
isSortable=true
width="200px"
)
(hash
key="value"
label="Value"
isSortable=true
)
}}
>
<:body as |B|>
<B.Tr data-test-var={{B.data.key}}>
<B.Td>{{B.data.key}}</B.Td>
<B.Td class="value-cell">
<div>
<CopyButton
@compact={{true}}
@clipboardText={{row.model.value}}
@clipboardText={{B.data.value}}
/>
<button
class="show-hide-values button is-borderless is-compact"
type="button"
{{on "click" (action this.toggleRowVisibility row.model)}}
{{on "click" (action this.toggleRowVisibility B.data)}}
{{keyboard-shortcut
label="Toggle Variable Visibility"
pattern=(array "v")
action=(action this.toggleRowVisibility row.model)
action=(action this.toggleRowVisibility B.data)
}}
>
<FlightIcon
@name={{if row.model.isVisible "eye" "eye-off"}}
@title={{if row.model.isVisible "Hide Value" "Show Value"}}
@name={{if B.data.isVisible "eye" "eye-off"}}
@title={{if B.data.isVisible "Hide Value" "Show Value"}}
/>
</button>
{{#if row.model.isVisible}}
<code>{{row.model.value}}</code>
{{#if B.data.isVisible}}
<code>{{B.data.value}}</code>
{{else}}
********
{{/if}}
</div>
</td>
</tr>
</t.body>
</ListTable>
</B.Td>
</B.Tr>
</:body>
</Hds::Table>
{{/if}}

View File

@@ -13,10 +13,7 @@ import {
visit,
} from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import {
selectChoose,
clickTrigger,
} from 'ember-power-select/test-support/helpers';
import { clickToggle, clickOption } from 'nomad-ui/tests/helpers/helios';
import { setupApplicationTest } from 'ember-qunit';
import { module, test } from 'qunit';
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
@@ -365,14 +362,13 @@ module('Acceptance | variables', function (hooks) {
window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId;
await Variables.visitNew();
assert.equal(currentURL(), '/variables/new');
await typeIn('.path-input', 'foo/bar');
await typeIn('[data-test-path-input]', 'foo/bar');
await click('button[type="submit"]');
assert.dom('.flash-message.alert-critical').exists();
await click('.flash-message.alert-critical .hds-dismiss-button');
assert.dom('.flash-message.alert-critical').doesNotExist();
await typeIn('.key-value label:nth-child(1) input', 'myKey');
await typeIn('.key-value label:nth-child(2) input', 'superSecret');
await typeIn('[data-test-var-key]', 'myKey');
await typeIn('[data-test-var-value]', 'superSecret');
await percySnapshot(assert);
@@ -412,9 +408,12 @@ module('Acceptance | variables', function (hooks) {
assert.equal(currentRouteName(), 'variables.new');
await typeIn('[data-test-path-input]', 'foo/bar');
await clickTrigger('[data-test-variable-namespace-filter]');
assert.dom('.dropdown-options').exists('Namespace can be edited.');
await clickToggle('[data-test-variable-namespace-filter]');
assert
.dom(
'[data-test-variable-namespace-filter] .hds-menu-primitive__content'
)
.exists('Namespace can be edited.');
assert
.dom('[data-test-variable-namespace-filter]')
.containsText(
@@ -422,10 +421,7 @@ module('Acceptance | variables', function (hooks) {
'The first alphabetically sorted namespace should be selected as the default option.'
);
await selectChoose(
'[data-test-variable-namespace-filter]',
'namespace-1'
);
await clickOption('[data-test-variable-namespace-filter]', 'namespace-1');
await typeIn('[data-test-var-key]', 'kiki');
await typeIn('[data-test-var-value]', 'do you love me');
await click('[data-test-submit-var]');
@@ -555,7 +551,7 @@ module('Acceptance | variables', function (hooks) {
.dom('.related-entities-hint')
.doesNotExist('Hides the hint when editing a job template variable');
assert
.dom('.job-template-hint')
.dom('[data-test-job-template-hint]')
.exists('Shows a hint about job templates');
assert
.dom('.CodeMirror')
@@ -574,7 +570,7 @@ module('Acceptance | variables', function (hooks) {
module('edit flow', function () {
test('allows a user with correct permissions to edit a variable', async function (assert) {
assert.expect(8);
assert.expect(7);
// Arrange Test Set-up
allScenarios.variableTestCluster(server);
server.createList('variable', 3);
@@ -603,10 +599,6 @@ module('Acceptance | variables', function (hooks) {
await percySnapshot(assert);
assert.dom('[data-test-path-input]').isDisabled('Path cannot be edited');
await clickTrigger('[data-test-variable-namespace-filter]');
assert
.dom('.dropdown-options')
.doesNotExist('Namespace cannot be edited.');
document.querySelector('[data-test-var-key]').value = ''; // clear current input
await typeIn('[data-test-var-key]', 'kiki');
@@ -884,8 +876,8 @@ module('Acceptance | variables', function (hooks) {
});
// Act
await clickTrigger('[data-test-variable-namespace-filter]');
await selectChoose('[data-test-variable-namespace-filter]', 'default');
await clickToggle('[data-test-variable-namespace-filter]');
await clickOption('[data-test-variable-namespace-filter]', 'default');
assert
.dom('[data-test-no-matching-variables-list-headline]')
@@ -946,8 +938,8 @@ module('Acceptance | variables', function (hooks) {
});
// Act
await clickTrigger('[data-test-variable-namespace-filter]');
await selectChoose('[data-test-variable-namespace-filter]', 'default');
await clickToggle('[data-test-variable-namespace-filter]');
await clickOption('[data-test-variable-namespace-filter]', 'default');
assert
.dom('[data-test-no-matching-variables-list-headline]')

View File

@@ -0,0 +1,38 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
// @ts-check
import {
click,
// fillIn,
// triggerKeyEvent,
// triggerEvent,
} from '@ember/test-helpers';
/**
* @param {string} scope
* @param {*} options
*/
export async function clickToggle(scope, options) {
let selector = '.hds-dropdown-toggle-button';
if (scope) {
selector = `${scope} ${selector}`;
}
return click(selector, options);
}
/**
* @param {string} scope
* @param {string} option the name of the option to click
* @param {*} options
*/
export async function clickOption(scope, option, options) {
let selector = `.hds-dropdown-list-item label input[name="${option}"]`;
if (scope) {
selector = `${scope} ${selector}`;
}
return click(selector, options);
}

View File

@@ -12,10 +12,8 @@ import { setupMirage } from 'ember-cli-mirage/test-support';
import setupCodeMirror from 'nomad-ui/tests/helpers/codemirror';
import { codeFillable, code } from 'nomad-ui/tests/pages/helpers/codemirror';
import percySnapshot from '@percy/ember';
import {
selectChoose,
clickTrigger,
} from 'ember-power-select/test-support/helpers';
import { clickToggle, clickOption } from 'nomad-ui/tests/helpers/helios';
import faker from 'nomad-ui/mirage/faker';
module('Integration | Component | variable-form', function (hooks) {
@@ -57,7 +55,7 @@ module('Integration | Component | variable-form', function (hooks) {
'The "Add More" button is disabled until key and value are filled'
);
await typeIn('.key-value label:nth-child(1) input', 'foo');
await typeIn('[data-test-var-key]', 'foo');
assert
.dom('[data-test-add-kv]')
@@ -65,7 +63,7 @@ module('Integration | Component | variable-form', function (hooks) {
'The "Add More" button is still disabled with only key filled'
);
await typeIn('.key-value label:nth-child(2) input', 'bar');
await typeIn('[data-test-var-value]', 'bar');
assert
.dom('[data-test-add-kv]')
@@ -81,9 +79,8 @@ module('Integration | Component | variable-form', function (hooks) {
'A second KV row exists after adding a new one'
);
await typeIn('.key-value:last-of-type label:nth-child(1) input', 'foo');
await typeIn('.key-value:last-of-type label:nth-child(2) input', 'bar');
await typeIn('.key-value:last-of-type [data-test-var-key]', 'foo');
await typeIn('.key-value:last-of-type [data-test-var-value]', 'bar');
await click('[data-test-add-kv]');
assert.equal(
@@ -92,7 +89,7 @@ module('Integration | Component | variable-form', function (hooks) {
'A third KV row exists after adding a new one'
);
await click('.key-value button.delete-row');
await click('.delete-entry-button');
assert.equal(
findAll('div.key-value').length,
@@ -116,37 +113,33 @@ module('Integration | Component | variable-form', function (hooks) {
await render(hbs`<VariableForm @model={{this.mockedModel}} />`);
await click('[data-test-add-kv]'); // add a second variable
findAll('input.value-input').forEach((input, iter) => {
assert.equal(
input.getAttribute('type'),
'password',
findAll('.value-label').forEach((label, iter) => {
const maskedInput = label.querySelector('.hds-form-masked-input');
assert.ok(
maskedInput.classList.contains('hds-form-masked-input--is-masked'),
`Value ${iter + 1} is hidden by default`
);
});
await click('.key-value button.show-hide-values');
const [firstRow, secondRow] = findAll('input.value-input');
await click('.hds-form-visibility-toggle');
const [firstRow, secondRow] = findAll('.hds-form-masked-input');
assert.equal(
firstRow.getAttribute('type'),
'text',
assert.ok(
firstRow.classList.contains('hds-form-masked-input--is-not-masked'),
'Only the row that is clicked on toggles visibility'
);
assert.equal(
secondRow.getAttribute('type'),
'password',
assert.ok(
secondRow.classList.contains('hds-form-masked-input--is-masked'),
'Rows that are not clicked remain obscured'
);
await click('.key-value button.show-hide-values');
assert.equal(
firstRow.getAttribute('type'),
'password',
await click('.hds-form-visibility-toggle');
assert.ok(
firstRow.classList.contains('hds-form-masked-input--is-masked'),
'Only the row that is clicked on toggles visibility'
);
assert.equal(
secondRow.getAttribute('type'),
'password',
assert.ok(
secondRow.classList.contains('hds-form-masked-input--is-masked'),
'Rows that are not clicked remain obscured'
);
await percySnapshot(assert);
@@ -177,7 +170,7 @@ module('Integration | Component | variable-form', function (hooks) {
'Shows 5 existing key values'
);
assert.equal(
findAll('button.delete-row').length,
findAll('.delete-entry-button').length,
5,
'Shows "delete" for all five rows'
);
@@ -189,13 +182,13 @@ module('Integration | Component | variable-form', function (hooks) {
findAll('div.key-value').forEach((row, idx) => {
assert.equal(
row.querySelector(`label:nth-child(1) input`).value,
row.querySelector(`[data-test-var-key]`).value,
keyValues[idx].key,
`Key ${idx + 1} is correct`
);
assert.equal(
row.querySelector(`label:nth-child(2) input`).value,
row.querySelector(`[data-test-var-value]`).value,
keyValues[idx].value,
keyValues[idx].value
);
@@ -214,9 +207,9 @@ module('Integration | Component | variable-form', function (hooks) {
variable.isNew = false;
this.set('variable', variable);
await render(hbs`<VariableForm @model={{this.variable}} />`);
assert.dom('input.path-input').hasValue('/baz/bat', 'Path is set');
assert.dom('[data-test-path-input]').hasValue('/baz/bat', 'Path is set');
assert
.dom('input.path-input')
.dom('[data-test-path-input]')
.isDisabled('Existing variable is in disabled state');
variable.isNew = true;
@@ -224,7 +217,7 @@ module('Integration | Component | variable-form', function (hooks) {
this.set('variable', variable);
await render(hbs`<VariableForm @model={{this.variable}} />`);
assert
.dom('input.path-input')
.dom('[data-test-path-input]')
.isNotDisabled('New variable is not in disabled state');
});
@@ -254,27 +247,36 @@ module('Integration | Component | variable-form', function (hooks) {
hbs`<VariableForm @model={{this.mockedModel}} @existingVariables={{this.existingVariables}} />`
);
await typeIn('.path-input', 'foo/bar');
assert.dom('.duplicate-path-error').doesNotExist();
assert.dom('.path-input').doesNotHaveClass('error');
await typeIn('[data-test-path-input]', 'foo/bar');
assert.dom('[data-test-duplicate-variable-error]').doesNotExist();
assert
.dom('[data-test-path-input]')
.doesNotHaveClass('hds-form-text-input--is-invalid');
document.querySelector('.path-input').value = ''; // clear current input
await typeIn('.path-input', 'baz/bat');
assert.dom('.duplicate-path-error').exists();
assert.dom('.path-input').hasClass('error');
document.querySelector('[data-test-path-input]').value = ''; // clear current input
await typeIn('[data-test-path-input]', 'baz/bat');
await clickTrigger('[data-test-variable-namespace-filter]');
await selectChoose(
assert.dom('[data-test-duplicate-variable-error]').exists();
assert
.dom('[data-test-path-input]')
.hasClass('hds-form-text-input--is-invalid');
await clickToggle('[data-test-variable-namespace-filter]');
await clickOption(
'[data-test-variable-namespace-filter]',
server.db.namespaces[2].id
);
assert.dom('.duplicate-path-error').doesNotExist();
assert.dom('.path-input').doesNotHaveClass('error');
assert.dom('[data-test-duplicate-variable-error]').doesNotExist();
assert
.dom('[data-test-path-input]')
.doesNotHaveClass('hds-form-text-input--is-invalid');
document.querySelector('.path-input').value = ''; // clear current input
await typeIn('.path-input', 'baz/bat/qux');
assert.dom('.duplicate-path-error').exists();
assert.dom('.path-input').hasClass('error');
document.querySelector('[data-test-path-input]').value = ''; // clear current input
await typeIn('[data-test-path-input]', 'baz/bat/qux');
assert.dom('[data-test-duplicate-variable-error]').exists();
assert
.dom('[data-test-path-input]')
.hasClass('hds-form-text-input--is-invalid');
});
test('warns you when you set a key with . in it', async function (assert) {
@@ -424,11 +426,8 @@ module('Integration | Component | variable-form', function (hooks) {
await click('[data-test-add-kv]');
await typeIn('.key-value:last-of-type label:nth-child(1) input', 'howdy');
await typeIn(
'.key-value:last-of-type label:nth-child(2) input',
'partner'
);
await typeIn('.key-value:last-of-type [data-test-var-key]', 'howdy');
await typeIn('.key-value:last-of-type [data-test-var-value]', 'partner');
this.set('view', 'json');
@@ -459,13 +458,13 @@ module('Integration | Component | variable-form', function (hooks) {
);
this.set('view', 'table');
assert.equal(
find(`.key-value:last-of-type label:nth-child(1) input`).value,
find(`.key-value:last-of-type [data-test-var-key]`).value,
'golden',
'Key persists from JSON to Table'
);
assert.equal(
find(`.key-value:last-of-type label:nth-child(2) input`).value,
find(`.key-value:last-of-type [data-test-var-value]`).value,
'gate',
'Value persists from JSON to Table'
);