Skip to content

Commit 44c71ad

Browse files
committed
[VAULT-35183] UI: enable search for updated namespace picker (#30292)
1 parent 4f4d070 commit 44c71ad

File tree

4 files changed

+320
-14
lines changed

4 files changed

+320
-14
lines changed

ui/app/components/namespace-picker.hbs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,23 @@
88

99
<D.ToggleButton @icon="org" @text={{or this.selected.id "-"}} data-test-namespace-toggle />
1010

11+
<D.Generic>
12+
<Hds::Form::TextInput::Field
13+
@value={{this.searchInput}}
14+
@type="search"
15+
placeholder="Search"
16+
{{on "input" this.onSearchInput}}
17+
/>
18+
</D.Generic>
19+
20+
<D.Separator />
21+
1122
<D.Generic>
1223
{{this.namespaceLabel}}
13-
<Hds::BadgeCount @text={{or this.options.length 0}} />
24+
<Hds::BadgeCount @text={{or this.namespaceOptions.length 0}} />
1425
</D.Generic>
1526

16-
{{#each this.options as |option|}}
27+
{{#each this.namespaceOptions as |option|}}
1728
<D.Checkmark
1829
@selected={{eq option.id this.selected.id}}
1930
{{on "click" (fn this.onChange option)}}
@@ -26,7 +37,7 @@
2637
{{! Check if the user has permissions to list namespaces. If true, display additional footer options like "Refresh list" and "Manage". }}
2738
{{#if this.hasListPermissions}}
2839
<D.Footer @hasDivider={{true}}>
29-
<Hds::Button @color="secondary" @text="Refresh list" {{on "click" this.loadOptions}} data-test-refresh-namespaces />
40+
<Hds::Button @color="secondary" @text="Refresh list" {{on "click" this.refreshList}} data-test-refresh-namespaces />
3041
<Hds::Button @color="secondary" @text="Manage" @icon="settings" @route="vault.cluster.access.namespaces" />
3142
</D.Footer>
3243
{{/if}}

ui/app/components/namespace-picker.js

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,17 @@ import { service } from '@ember/service';
2020
*/
2121

2222
export default class NamespacePicker extends Component {
23+
@service auth;
2324
@service namespace;
2425
@service router;
2526
@service store;
2627

2728
// Show/hide refresh & manage namespaces buttons
2829
@tracked hasListPermissions = false;
2930

30-
@tracked selected = {};
31-
@tracked options = [];
31+
@tracked allNamespaces = [];
3232
@tracked searchInput = '';
33+
@tracked selected = {};
3334

3435
constructor() {
3536
super(...arguments);
@@ -63,20 +64,42 @@ export default class NamespacePicker extends Component {
6364
* | 'parent' | 'parent' | 'parent' |
6465
* | 'child' | 'parent/child' | 'parent/child' |
6566
*/
66-
return [
67-
// TODO: Some users (including HDS Admin User) should never see the root namespace. Address this in a followup PR.
68-
{ id: 'root', path: '', label: 'root' },
67+
const options = [
6968
...(namespace?.accessibleNamespaces || []).map((ns) => {
7069
const parts = ns.split('/');
7170
return { id: parts[parts.length - 1], path: ns, label: ns };
7271
}),
7372
];
73+
74+
// Conditionally add the root namespace
75+
if (this.auth.authData.userRootNamespace === '') {
76+
options.unshift({ id: 'root', path: '', label: 'root' });
77+
}
78+
79+
return options;
7480
}
7581

7682
get namespaceLabel() {
7783
return this.searchInput === '' ? 'All namespaces' : 'Matching namespaces';
7884
}
7985

86+
get namespaceOptions() {
87+
if (this.searchInput.trim() === '') {
88+
// If the search input is empty, reset to all namespaces
89+
return this.allNamespaces;
90+
} else {
91+
// Filter namespaces based on the search input
92+
return this.allNamespaces.filter((ns) =>
93+
ns.label.toLowerCase().includes(this.searchInput.toLowerCase())
94+
);
95+
}
96+
}
97+
98+
@action
99+
onSearchInput(event) {
100+
this.searchInput = event.target.value;
101+
}
102+
80103
@action
81104
async fetchListCapability() {
82105
// TODO: Revist. This logic was carried over from previous component implmenetation.
@@ -98,12 +121,18 @@ export default class NamespacePicker extends Component {
98121
// Check with design to determine if we should continue to ignore or handle an error situation here.
99122
await this.namespace?.findNamespacesForUser.perform();
100123

101-
this.options = this.#getOptions(this.namespace);
102-
this.selected = this.#getSelected(this.options, this.namespace);
124+
this.allNamespaces = this.#getOptions(this.namespace);
125+
this.selected = this.#getSelected(this.allNamespaces, this.namespace);
103126

104127
await this.fetchListCapability();
105128
}
106129

130+
@action
131+
async refreshList() {
132+
this.searchInput = '';
133+
await this.loadOptions();
134+
}
135+
107136
@action
108137
async onChange(selected) {
109138
this.selected = selected;

ui/tests/acceptance/enterprise-namespaces-test.js

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* SPDX-License-Identifier: BUSL-1.1
44
*/
55

6-
import { click, settled, visit, fillIn, currentURL, waitFor } from '@ember/test-helpers';
6+
import { click, settled, visit, fillIn, currentURL, waitFor, findAll } from '@ember/test-helpers';
77
import { module, test } from 'qunit';
88
import { setupApplicationTest } from 'ember-qunit';
99
import { runCmd, createNS } from 'vault/tests/helpers/commands';
@@ -12,13 +12,112 @@ import { AUTH_FORM } from 'vault/tests/helpers/auth/auth-form-selectors';
1212
import { GENERAL } from '../helpers/general-selectors';
1313
import { NAMESPACE_PICKER_SELECTORS } from '../helpers/namespace-picker';
1414

15+
import sinon from 'sinon';
16+
1517
module('Acceptance | Enterprise | namespaces', function (hooks) {
1618
setupApplicationTest(hooks);
1719

18-
hooks.beforeEach(function () {
20+
let fetchSpy;
21+
22+
hooks.beforeEach(() => {
23+
fetchSpy = sinon.spy(window, 'fetch');
1924
return login();
2025
});
2126

27+
hooks.afterEach(() => {
28+
fetchSpy.restore();
29+
});
30+
31+
test('it filters namespaces based on search input', async function (assert) {
32+
assert.expect(7);
33+
34+
await click(NAMESPACE_PICKER_SELECTORS.toggle);
35+
36+
// Verify all namespaces are displayed initially
37+
assert.dom(NAMESPACE_PICKER_SELECTORS.link()).exists('Namespace link(s) exist');
38+
assert.strictEqual(
39+
findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
40+
5,
41+
'All namespaces are displayed initially'
42+
);
43+
44+
// Verify the search input field exists
45+
assert.dom('[type="search"]').exists('The namespace search field exists');
46+
47+
// Verify 3 namespaces are displayed after searching for "beep"
48+
await fillIn('[type="search"]', 'beep');
49+
assert.strictEqual(
50+
findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
51+
3,
52+
'Display 3 namespaces matching "beep" after searching'
53+
);
54+
55+
// Verify 1 namespace is displayed after searching for "bop"
56+
await fillIn('[type="search"]', 'bop');
57+
assert.strictEqual(
58+
findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
59+
1,
60+
'Display 1 namespace matching "bop" after searching'
61+
);
62+
63+
// Verify no namespaces are displayed after searching for "other"
64+
await fillIn('[type="search"]', 'other');
65+
assert.strictEqual(
66+
findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
67+
0,
68+
'No namespaces are displayed after searching for "other"'
69+
);
70+
71+
// Clear the search input & verify all namespaces are displayed again
72+
await fillIn('[type="search"]', '');
73+
assert.strictEqual(
74+
findAll(NAMESPACE_PICKER_SELECTORS.link()).length,
75+
5,
76+
'All namespaces are displayed after clearing search input'
77+
);
78+
});
79+
80+
test('it updates the namespace list after clicking "Refresh list"', async function (assert) {
81+
assert.expect(3);
82+
83+
await click(NAMESPACE_PICKER_SELECTORS.toggle);
84+
85+
// Verify that the namespace list was fetched on load
86+
let listNamespaceRequests = fetchSpy
87+
.getCalls()
88+
.filter((call) => call.args[0].includes('/v1/sys/internal/ui/namespaces'));
89+
assert.strictEqual(
90+
listNamespaceRequests.length,
91+
1,
92+
'The network call to the specific endpoint was made twice (once on load, once on refresh)'
93+
);
94+
95+
// Refresh the list of namespaces
96+
assert.dom(NAMESPACE_PICKER_SELECTORS.refreshList).exists('Refresh list button exists');
97+
await click(NAMESPACE_PICKER_SELECTORS.refreshList);
98+
99+
// Verify that the namespace list was fetched on refresh
100+
listNamespaceRequests = fetchSpy
101+
.getCalls()
102+
.filter((call) => call.args[0].includes('/v1/sys/internal/ui/namespaces'));
103+
assert.strictEqual(
104+
listNamespaceRequests.length,
105+
2,
106+
'The network call to the specific endpoint was made twice (once on load, once on refresh)'
107+
);
108+
});
109+
110+
test('it displays the "Manage" button with the correct URL', async function (assert) {
111+
assert.expect(1);
112+
113+
await click(NAMESPACE_PICKER_SELECTORS.toggle);
114+
115+
// Verify the "Manage" button is rendered and has the correct URL
116+
assert
117+
.dom('[href="/ui/vault/access/namespaces"]')
118+
.exists('The "Manage" button is displayed with the correct URL');
119+
});
120+
22121
test('it clears namespaces when you log out', async function (assert) {
23122
const ns = 'foo';
24123
await runCmd(createNS(ns), false);
@@ -31,7 +130,7 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
31130
.exists('The root namespace is selected');
32131
});
33132

34-
// TODO: Is this test description still accurate?
133+
// TODO: revisit test name/description, is this still relevant? A '/' prefix is stripped from namespace on login form
35134
test('it shows nested namespaces if you log in with a namespace starting with a /', async function (assert) {
36135
assert.expect(6);
37136

@@ -41,17 +140,24 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
41140
for (const [i, ns] of nses.entries()) {
42141
await runCmd(createNS(ns), false);
43142
await settled();
143+
44144
// the namespace path will include all of the namespaces up to this point
45145
const targetNamespace = nses.slice(0, i + 1).join('/');
46146
const url = `/vault/secrets?namespace=${targetNamespace}`;
147+
47148
// this is usually triggered when creating a ns in the form -- trigger a reload of the namespaces manually
48149
await click(NAMESPACE_PICKER_SELECTORS.toggle);
150+
151+
// refresh the list of namespaces
152+
await waitFor(NAMESPACE_PICKER_SELECTORS.refreshList);
49153
await click(NAMESPACE_PICKER_SELECTORS.refreshList);
50-
await waitFor(NAMESPACE_PICKER_SELECTORS.link(targetNamespace));
154+
51155
// check that the full namespace path, like "beep/boop", shows in the toggle display
156+
await waitFor(NAMESPACE_PICKER_SELECTORS.link(targetNamespace));
52157
assert
53158
.dom(NAMESPACE_PICKER_SELECTORS.link(targetNamespace))
54159
.hasText(targetNamespace, `shows the namespace ${targetNamespace} in the toggle component`);
160+
55161
// because quint does not like page reloads, visiting url directly instead of clicking on namespace in toggle
56162
await visit(url);
57163
}

0 commit comments

Comments
 (0)