mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
UI: Fix namespace picker selecting all namespaces with matching nodes (#31326)
* add test * add changelog
This commit is contained in:
parent
4c828c389d
commit
41d8301927
4 changed files with 47 additions and 34 deletions
3
changelog/31326.txt
Normal file
3
changelog/31326.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:bug
|
||||
ui: Fix selecting multiple namespaces in the namespace picker when the path contains matching nodes
|
||||
```
|
||||
|
|
@ -8,7 +8,9 @@
|
|||
|
||||
<D.ToggleButton
|
||||
@icon="org"
|
||||
@text={{or this.selectedNamespace.id "-"}}
|
||||
{{! Displays the node of the current namespace context in the toggle }}
|
||||
{{! For example, if a user navigates to 'parent/child' the toggle just displays 'child' }}
|
||||
@text={{this.namespace.currentNamespace}}
|
||||
@isFullWidth={{true}}
|
||||
data-test-button="namespace-picker"
|
||||
{{on "click" this.toggleNamespacePicker}}
|
||||
|
|
@ -59,7 +61,7 @@
|
|||
<div class="is-overflow-y-auto is-max-drawer-height" {{did-insert this.setupScrollListener}}>
|
||||
{{#each this.visibleNamespaceOptions as |option|}}
|
||||
<D.Checkmark
|
||||
@selected={{eq option.id this.selectedNamespace.id}}
|
||||
@selected={{eq option.path this.selectedNamespace.path}}
|
||||
{{on "click" (fn this.onChange option)}}
|
||||
data-test-button={{option.label}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import type Store from '@ember-data/store';
|
|||
import errorMessage from 'vault/utils/error-message';
|
||||
|
||||
interface NamespaceOption {
|
||||
id: string;
|
||||
path: string;
|
||||
label: string;
|
||||
}
|
||||
|
|
@ -56,11 +55,7 @@ export default class NamespacePicker extends Component {
|
|||
}
|
||||
|
||||
get allNamespaces(): NamespaceOption[] {
|
||||
return this.getOptions(
|
||||
this.namespace?.accessibleNamespaces,
|
||||
this.namespace?.currentNamespace,
|
||||
this.namespace?.path
|
||||
);
|
||||
return this.getOptions(this.namespace?.accessibleNamespaces);
|
||||
}
|
||||
|
||||
get selectedNamespace(): NamespaceOption | null {
|
||||
|
|
@ -75,45 +70,33 @@ export default class NamespacePicker extends Component {
|
|||
return options.find((option) => this.matchesPath(option, currentPath));
|
||||
}
|
||||
|
||||
private getOptions(
|
||||
accessibleNamespaces: string[],
|
||||
currentNamespace: string,
|
||||
path: string
|
||||
): NamespaceOption[] {
|
||||
/* Each namespace option has 3 properties: { id, path, and label }
|
||||
* - id: node / namespace name (displayed when the namespace picker is closed)
|
||||
private getOptions(accessibleNamespaces: string[]): NamespaceOption[] {
|
||||
/* Each namespace option has 2 properties: { path and label }
|
||||
* - path: full namespace path (used to navigate to the namespace)
|
||||
* - label: text displayed inside the namespace picker dropdown (if root, then label = id, else label = path)
|
||||
* - label: text displayed inside the namespace picker dropdown (if root, then path is "", else label = path)
|
||||
*
|
||||
* Example:
|
||||
* | id | path | label |
|
||||
* | --- | ---- | ----- |
|
||||
* | 'root' | '' | 'root' |
|
||||
* | 'parent' | 'parent' | 'parent' |
|
||||
* | 'child' | 'parent/child' | 'parent/child' |
|
||||
* | path | label |
|
||||
* | ---- | ----- |
|
||||
* | '' | 'root' |
|
||||
* | 'parent' | 'parent' |
|
||||
* | 'parent/child' | 'parent/child' |
|
||||
*/
|
||||
const options = [
|
||||
...(accessibleNamespaces || []).map((ns: string) => {
|
||||
const parts = ns.split('/');
|
||||
return { id: parts[parts.length - 1] || '', path: ns, label: ns };
|
||||
}),
|
||||
];
|
||||
const options = (accessibleNamespaces || []).map((ns: string) => ({ path: ns, label: ns }));
|
||||
|
||||
// Add the user's root namespace because `sys/internal/ui/namespaces` does not include it.
|
||||
const userRootNamespace = this.auth.authData?.userRootNamespace;
|
||||
if (!options?.find((o) => o.path === userRootNamespace)) {
|
||||
const ns = userRootNamespace === '' ? 'root' : userRootNamespace;
|
||||
options.unshift({ id: ns, path: userRootNamespace, label: ns });
|
||||
// the 'root' namespace is technically an empty string so we manually add the 'root' label.
|
||||
const label = userRootNamespace === '' ? 'root' : userRootNamespace;
|
||||
options.unshift({ path: userRootNamespace, label });
|
||||
}
|
||||
|
||||
// If there are no namespaces returned by the internal endpoint, add the current namespace
|
||||
// to the list of options. This is a fallback for when the user has access to a single namespace.
|
||||
if (options.length === 0) {
|
||||
options.push({
|
||||
id: currentNamespace,
|
||||
path: path,
|
||||
label: path,
|
||||
});
|
||||
// 'path' defined in the namespace service is the full namespace path
|
||||
options.push({ path: this.namespace.path, label: this.namespace.path });
|
||||
}
|
||||
|
||||
return options;
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@ module('Integration | Component | namespace-picker', function (hooks) {
|
|||
);
|
||||
});
|
||||
|
||||
test('it selects the current namespace', async function (assert) {
|
||||
await render(hbs`<NamespacePicker />`);
|
||||
assert.dom(GENERAL.button('namespace-picker')).hasText('child1', 'it just displays the namespace node');
|
||||
await click(GENERAL.button('namespace-picker'));
|
||||
assert
|
||||
.dom(GENERAL.button(this.nsService.path))
|
||||
.hasAttribute('aria-selected', 'true', 'the current namespace path is selected');
|
||||
assert.dom(`${GENERAL.button(this.nsService.path)} ${GENERAL.icon('check')}`).exists();
|
||||
});
|
||||
|
||||
test('it filters namespace options based on search input', async function (assert) {
|
||||
await render(hbs`<NamespacePicker/>`);
|
||||
await click(GENERAL.button('namespace-picker'));
|
||||
|
|
@ -160,4 +170,19 @@ module('Integration | Component | namespace-picker', function (hooks) {
|
|||
assert.dom(GENERAL.button('admin')).exists();
|
||||
assert.dom(GENERAL.button('admin/child1')).exists();
|
||||
});
|
||||
|
||||
test('it selects the correct namespace when matching nodes exist', async function (assert) {
|
||||
// stub response so that two namespaces have matching node names 'child1'
|
||||
this.server.get('/sys/internal/ui/namespaces', () => {
|
||||
return { data: { keys: ['parent1/', 'parent1/child1', 'anotherParent/', 'anotherParent/child1'] } };
|
||||
});
|
||||
await render(hbs`<NamespacePicker />`);
|
||||
assert.dom(GENERAL.button('namespace-picker')).hasText('child1', 'it displays the namespace node');
|
||||
await click(GENERAL.button('namespace-picker'));
|
||||
assert
|
||||
.dom(GENERAL.button(this.nsService.path))
|
||||
.hasAttribute('aria-selected', 'true', 'the current namespace path is selected');
|
||||
assert.dom('[aria-selected="true"]').exists({ count: 1 }, 'only one option is selected');
|
||||
assert.dom(GENERAL.icon('check')).exists({ count: 1 }, 'only one check mark renders');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue