mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 09:42:25 +00:00
Address a11y issues in browser-based console UI (#26872)
This commit is contained in:
3
changelog/26872.txt
Normal file
3
changelog/26872.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:bug
|
||||
ui: Resolved accessibility issues with Web REPL. Associated label and help text with input, added a conditional to show the console/ui-panel only when toggled open, added keyboard focus trap.
|
||||
```
|
||||
@@ -17,16 +17,17 @@ export default Component.extend({
|
||||
actions: {
|
||||
handleKeyUp(event) {
|
||||
const keyCode = event.keyCode;
|
||||
const val = event.target.value;
|
||||
switch (keyCode) {
|
||||
case keys.ENTER:
|
||||
this.onExecuteCommand(event.target.value);
|
||||
this.onExecuteCommand(val);
|
||||
break;
|
||||
case keys.UP:
|
||||
case keys.DOWN:
|
||||
this.onShiftCommand(keyCode);
|
||||
break;
|
||||
default:
|
||||
this.onValueUpdate(event.target.value);
|
||||
this.onValueUpdate(val);
|
||||
}
|
||||
},
|
||||
fullscreen() {
|
||||
|
||||
@@ -49,7 +49,14 @@
|
||||
<LinkStatus @status={{this.currentCluster.cluster.hcpLinkStatus}} />
|
||||
{{yield}}
|
||||
<div data-test-console-panel class={{if this.console.isOpen "panel-open"}}>
|
||||
<Console::UiPanel @isFullscreen={{this.consoleFullscreen}} />
|
||||
{{#if this.console.isOpen}}
|
||||
<Console::UiPanel
|
||||
@isFullscreen={{this.consoleFullscreen}}
|
||||
{{focus-trap
|
||||
focusTrapOptions=(hash initialFocus="#console-input" clickOutsideDeactivates=true onDeactivate=this.closeConsole)
|
||||
}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
</Frame.Main>
|
||||
</Hds::AppFrame>
|
||||
@@ -14,7 +14,9 @@ $console-close-height: 35px;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
transition: min-height $speed $easing, transform $speed ease-in;
|
||||
transition:
|
||||
min-height $speed $easing,
|
||||
transform $speed ease-in;
|
||||
will-change: transform, min-height;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
z-index: 199;
|
||||
@@ -118,11 +120,11 @@ $console-close-height: 35px;
|
||||
|
||||
.panel-open .console-ui-panel {
|
||||
box-shadow: $box-shadow-highest;
|
||||
min-height: 400px;
|
||||
min-height: 425px;
|
||||
}
|
||||
|
||||
.main--console-open {
|
||||
padding-bottom: 400px;
|
||||
padding-bottom: 425px;
|
||||
}
|
||||
|
||||
.panel-open .console-ui-panel.fullscreen {
|
||||
|
||||
@@ -10,19 +10,20 @@
|
||||
<Chevron />
|
||||
{{/if}}
|
||||
<input
|
||||
aria-label="command input"
|
||||
aria-label="web R.E.P.L."
|
||||
aria-describedby="namespace-reminder"
|
||||
onkeyup={{action "handleKeyUp"}}
|
||||
value={{this.value}}
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
id="console-input"
|
||||
/>
|
||||
<Hds::Button
|
||||
class="hds-side-nav__icon-button"
|
||||
{{on "click" (action "fullscreen")}}
|
||||
{{hds-tooltip (if this.isFullscreen "minimize" "maximize")}}
|
||||
data-test-tool-tip-trigger
|
||||
data-test-dismiss-console-button
|
||||
@icon={{if this.isFullscreen "minimize" "maximize"}}
|
||||
@text={{if this.isFullscreen "Minimize" "Maximize"}}
|
||||
@text={{if this.isFullscreen "Minimize window" "Maximize window"}}
|
||||
@isIconOnly={{true}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,17 +3,19 @@
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<div class="console-close-button">
|
||||
<Hds::Button
|
||||
class="hds-side-nav__icon-button"
|
||||
{{on "click" (action "closeConsole")}}
|
||||
data-test-console-panel-close
|
||||
@text="Close console"
|
||||
@icon="x"
|
||||
@isIconOnly={{true}}
|
||||
/>
|
||||
</div>
|
||||
<div class="console-ui-panel-content">
|
||||
<div class="content has-bottom-margin-s">
|
||||
<div class="console-close-button">
|
||||
<Hds::Button
|
||||
class="hds-side-nav__icon-button"
|
||||
{{on "click" (action "closeConsole")}}
|
||||
data-test-console-panel-close
|
||||
@text="Close console"
|
||||
@icon="x"
|
||||
@isIconOnly={{true}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content has-bottom-margin-l">
|
||||
<p class="console-ui-panel-intro is-font-mono has-bottom-margin-s">
|
||||
The Vault Web REPL provides an easy way to execute common Vault CLI commands, such as write, read, delete, and list. It
|
||||
@@ -22,14 +24,18 @@
|
||||
<Hds::Link::Inline @href={{doc-link "/vault/docs/command/web"}}>HashiCorp Developer site</Hds::Link::Inline>.
|
||||
</p>
|
||||
<p class="console-ui-panel-intro is-font-mono has-bottom-margin-s">Examples:</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">→ Write secrets to kv v1: write <mount>/my-secret foo=bar</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">→ List kv v1 secret keys: list <mount>/</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">→ Read a kv v1 secret: read <mount>/my-secret</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">→ Mount a kv v2 secret engine: write sys/mounts/<mount> type=kv
|
||||
options=version=2</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">→ Read a kv v2 secret: kv-get <mount>/secret-path</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">→ Read a kv v2 secret's metadata: kv-get <mount>/secret-path
|
||||
-metadata</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">
|
||||
<span aria-hidden="true">→ </span>Write secrets to kv v1: write <mount>/my-secret foo=bar</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">
|
||||
<span aria-hidden="true">→ </span>List kv v1 secret keys: list <mount>/</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">
|
||||
<span aria-hidden="true">→ </span>Read a kv v1 secret: read <mount>/my-secret</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">
|
||||
<span aria-hidden="true">→ </span>Mount a kv v2 secret engine: write sys/mounts/<mount> type=kv options=version=2</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">
|
||||
<span aria-hidden="true">→ </span>Read a kv v2 secret: kv-get <mount>/secret-path</p>
|
||||
<p class="console-ui-panel-intro is-font-mono">
|
||||
<span aria-hidden="true">→ </span>Read a kv v2 secret's metadata: kv-get <mount>/secret-path-metadata</p>
|
||||
</div>
|
||||
<Console::OutputLog @outputLog={{this.cliLog}} />
|
||||
<Console::CommandInput
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
{{#if this.showMessage}}
|
||||
{{#if (has-block)}}
|
||||
<p class="namespace-reminder">
|
||||
<p class="namespace-reminder" id="namespace-reminder">
|
||||
{{yield (hash namespace=this.namespace)}}
|
||||
</p>
|
||||
{{else}}
|
||||
<p class="namespace-reminder">
|
||||
<p class="namespace-reminder" id="namespace-reminder">
|
||||
This
|
||||
{{@noun}}
|
||||
will be
|
||||
|
||||
@@ -25,8 +25,6 @@ module('Acceptance | console', function (hooks) {
|
||||
assert.expect(6);
|
||||
await enginesPage.visit();
|
||||
await settled();
|
||||
await consoleComponent.toggle();
|
||||
await settled();
|
||||
const ids = [uuidv4(), uuidv4(), uuidv4()];
|
||||
for (const id of ids) {
|
||||
const inputString = `write sys/mounts/console-route-${id} type=kv`;
|
||||
@@ -59,7 +57,7 @@ module('Acceptance | console', function (hooks) {
|
||||
test('fullscreen command expands the cli panel', async function (assert) {
|
||||
await consoleComponent.toggle();
|
||||
await settled();
|
||||
await consoleComponent.runCommands('fullscreen');
|
||||
await consoleComponent.runCommands('fullscreen', false);
|
||||
await settled();
|
||||
const consoleEle = document.querySelector('[data-test-component="console/ui-panel"]');
|
||||
// wait for the CSS transition to finish
|
||||
@@ -74,7 +72,7 @@ module('Acceptance | console', function (hooks) {
|
||||
test('array output is correctly formatted', async function (assert) {
|
||||
await consoleComponent.toggle();
|
||||
await settled();
|
||||
await consoleComponent.runCommands('read -field=policies /auth/token/lookup-self');
|
||||
await consoleComponent.runCommands('read -field=policies /auth/token/lookup-self', false);
|
||||
await settled();
|
||||
const consoleOut = document.querySelector('.console-ui-output>pre');
|
||||
// wait for the CSS transition to finish
|
||||
@@ -86,7 +84,7 @@ module('Acceptance | console', function (hooks) {
|
||||
test('number output is correctly formatted', async function (assert) {
|
||||
await consoleComponent.toggle();
|
||||
await settled();
|
||||
await consoleComponent.runCommands('read -field=creation_time /auth/token/lookup-self');
|
||||
await consoleComponent.runCommands('read -field=creation_time /auth/token/lookup-self', false);
|
||||
await settled();
|
||||
const consoleOut = document.querySelector('.console-ui-output>pre');
|
||||
// wait for the CSS transition to finish
|
||||
@@ -97,7 +95,7 @@ module('Acceptance | console', function (hooks) {
|
||||
test('boolean output is correctly formatted', async function (assert) {
|
||||
await consoleComponent.toggle();
|
||||
await settled();
|
||||
await consoleComponent.runCommands('read -field=orphan /auth/token/lookup-self');
|
||||
await consoleComponent.runCommands('read -field=orphan /auth/token/lookup-self', false);
|
||||
await settled();
|
||||
const consoleOut = document.querySelector('.console-ui-output>pre');
|
||||
// have to wrap in a later so that we can wait for the CSS transition to finish
|
||||
|
||||
@@ -111,6 +111,7 @@ module('Acceptance | pki workflow', function (hooks) {
|
||||
allow_subdomains=true \
|
||||
max_ttl="720h"`,
|
||||
]);
|
||||
|
||||
await runCmd([`write ${this.mountPath}/root/generate/internal common_name="Hashicorp Test"`]);
|
||||
const pki_admin_policy = adminPolicy(this.mountPath, 'roles');
|
||||
const pki_reader_policy = readerPolicy(this.mountPath, 'roles');
|
||||
|
||||
@@ -25,7 +25,12 @@ const visit = async (url) => {
|
||||
const consoleComponent = create(consoleClass);
|
||||
|
||||
const wrappedAuth = async () => {
|
||||
await consoleComponent.runCommands(`write -field=token auth/token/create policies=default -wrap-ttl=5m`);
|
||||
await consoleComponent.toggle();
|
||||
await settled();
|
||||
await consoleComponent.runCommands(
|
||||
`write -field=token auth/token/create policies=default -wrap-ttl=5m`,
|
||||
false
|
||||
);
|
||||
await settled();
|
||||
// because of flaky test, trying to capture the token using a dom selector instead of the page object
|
||||
const token = document.querySelector('[data-test-component="console/log-text"] pre').textContent;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
import { fillIn } from '@ember/test-helpers';
|
||||
import { fillIn, settled } from '@ember/test-helpers';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import enablePage from 'vault/tests/pages/settings/auth/enable';
|
||||
@@ -59,6 +59,8 @@ module('Acceptance | settings/auth/configure/section', function (hooks) {
|
||||
for (const type of ['aws', 'azure', 'gcp', 'github', 'kubernetes']) {
|
||||
test(`it shows tabs for auth method: ${type}`, async function (assert) {
|
||||
const path = `${type}-showtab-${this.uid}`;
|
||||
await cli.toggle();
|
||||
await settled();
|
||||
await cli.consoleInput(`write sys/auth/${path} type=${type}`);
|
||||
await cli.enter();
|
||||
await indexPage.visit({ path });
|
||||
|
||||
@@ -153,12 +153,16 @@ module('Acceptance | settings/mount-secret-backend', function (hooks) {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
`;
|
||||
await consoleComponent.runCommands([
|
||||
// delete any previous mount with same name
|
||||
`delete sys/mounts/${enginePath}`,
|
||||
`write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`,
|
||||
'write -field=client_token auth/token/create policies=kv-v2-degrade',
|
||||
]);
|
||||
await consoleComponent.toggle();
|
||||
await consoleComponent.runCommands(
|
||||
[
|
||||
// delete any previous mount with same name
|
||||
`delete sys/mounts/${enginePath}`,
|
||||
`write sys/policies/acl/kv-v2-degrade policy=${btoa(V2_POLICY)}`,
|
||||
'write -field=client_token auth/token/create policies=kv-v2-degrade',
|
||||
],
|
||||
false
|
||||
);
|
||||
await settled();
|
||||
const userToken = consoleComponent.lastLogOutput;
|
||||
await logout.visit();
|
||||
|
||||
@@ -14,7 +14,12 @@ import consoleClass from 'vault/tests/pages/components/console/ui-panel';
|
||||
const consoleComponent = create(consoleClass);
|
||||
|
||||
const wrappedAuth = async () => {
|
||||
await consoleComponent.runCommands(`write -field=token auth/token/create policies=default -wrap-ttl=3m`);
|
||||
await consoleComponent.toggle();
|
||||
await settled();
|
||||
await consoleComponent.runCommands(
|
||||
`write -field=token auth/token/create policies=default -wrap-ttl=3m`,
|
||||
false
|
||||
);
|
||||
await settled();
|
||||
return consoleComponent.lastLogOutput;
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import consoleClass from 'vault/tests/pages/components/console/ui-panel';
|
||||
import { create } from 'ember-cli-page-object';
|
||||
|
||||
/**
|
||||
* Helper functions to run common commands in the consoleComponent during tests.
|
||||
* Please note that a user must be logged in during the test context for the commands to run.
|
||||
@@ -45,8 +46,10 @@ export async function runCmd(commands, throwErrors = true) {
|
||||
if (!Array.isArray(commands)) {
|
||||
commands = [commands];
|
||||
}
|
||||
await cc.runCommands(commands);
|
||||
await cc.toggle();
|
||||
await cc.runCommands(commands, false);
|
||||
const lastOutput = cc.lastLogOutput;
|
||||
await cc.toggle();
|
||||
if (throwErrors && lastOutput.includes('Error')) {
|
||||
throw new Error(`Error occurred while running commands: "${commands.join('; ')}" - ${lastOutput}`);
|
||||
}
|
||||
|
||||
@@ -23,20 +23,21 @@ module('Integration | Component | console/ui panel', function (hooks) {
|
||||
|
||||
test('it clears console input on enter', async function (assert) {
|
||||
await render(hbs`{{console/ui-panel}}`);
|
||||
|
||||
await component.runCommands('list this/thing/here');
|
||||
await component.runCommands('list this/thing/here', false);
|
||||
await settled();
|
||||
assert.strictEqual(component.consoleInputValue, '', 'empties input field on enter');
|
||||
});
|
||||
|
||||
test('it clears the log when using clear command', async function (assert) {
|
||||
await render(hbs`{{console/ui-panel}}`);
|
||||
|
||||
await component.runCommands(['list this/thing/here', 'list this/other/thing', 'read another/thing']);
|
||||
await component.runCommands(
|
||||
['list this/thing/here', 'list this/other/thing', 'read another/thing'],
|
||||
false
|
||||
);
|
||||
await settled();
|
||||
assert.notEqual(component.logOutput, '', 'there is output in the log');
|
||||
|
||||
await component.runCommands('clear');
|
||||
await component.runCommands('clear', false);
|
||||
await settled();
|
||||
await component.up();
|
||||
await settled();
|
||||
@@ -51,7 +52,7 @@ module('Integration | Component | console/ui panel', function (hooks) {
|
||||
test('it adds command to history on enter', async function (assert) {
|
||||
await render(hbs`{{console/ui-panel}}`);
|
||||
|
||||
await component.runCommands('list this/thing/here');
|
||||
await component.runCommands('list this/thing/here', false);
|
||||
await settled();
|
||||
await component.up();
|
||||
await settled();
|
||||
@@ -67,8 +68,7 @@ module('Integration | Component | console/ui panel', function (hooks) {
|
||||
|
||||
test('it cycles through history with more than one command', async function (assert) {
|
||||
await render(hbs`{{console/ui-panel}}`);
|
||||
|
||||
await component.runCommands(['list this/thing/here', 'read that/thing/there', 'qwerty']);
|
||||
await component.runCommands(['list this/thing/here', 'read that/thing/there', 'qwerty'], false);
|
||||
await settled();
|
||||
await component.up();
|
||||
await settled();
|
||||
|
||||
@@ -35,7 +35,7 @@ module('Integration | Component | sidebar-frame', function (hooks) {
|
||||
assert.dom('[data-test-sidebar-nav]').doesNotExist('Sidebar is hidden');
|
||||
});
|
||||
|
||||
test('it should render link status, console ui panel and yield block for app content', async function (assert) {
|
||||
test('it should render link status, console ui panel container and yield block for app content', async function (assert) {
|
||||
const currentCluster = this.owner.lookup('service:currentCluster');
|
||||
currentCluster.setCluster({ hcpLinkStatus: 'connected' });
|
||||
const version = this.owner.lookup('service:version');
|
||||
@@ -50,7 +50,7 @@ module('Integration | Component | sidebar-frame', function (hooks) {
|
||||
`);
|
||||
|
||||
assert.dom('[data-test-link-status]').exists('Link status component renders');
|
||||
assert.dom('[data-test-component="console/ui-panel"]').exists('Console UI panel renders');
|
||||
assert.dom('[data-test-console-panel]').exists('Console UI panel container renders');
|
||||
assert.dom('.page-container').exists('Block yields for app content');
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import keys from 'core/utils/key-codes';
|
||||
|
||||
export default {
|
||||
toggle: clickable('[data-test-console-toggle]'),
|
||||
dismissConsole: clickable(['data-test-dismiss-console-button']),
|
||||
consoleInput: fillable('[data-test-component="console/command-input"] input'),
|
||||
consoleInputValue: value('[data-test-component="console/command-input"] input'),
|
||||
logOutput: text('[data-test-component="console/output-log"]'),
|
||||
@@ -54,12 +55,18 @@ export default {
|
||||
eventProperties: { keyCode: keys.ENTER },
|
||||
}),
|
||||
hasInput: isPresent('[data-test-component="console/command-input"] input'),
|
||||
runCommands: async function (commands) {
|
||||
runCommands: async function (commands, shouldToggle = true) {
|
||||
const toExecute = Array.isArray(commands) ? commands : [commands];
|
||||
if (shouldToggle) {
|
||||
await this.toggle(); // toggle the console open
|
||||
}
|
||||
for (const command of toExecute) {
|
||||
await this.consoleInput(command);
|
||||
await this.enter();
|
||||
await settled();
|
||||
}
|
||||
if (shouldToggle) {
|
||||
await this.toggle(); // toggle it closed
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user