Repository: hashicorp/vault
Pull request #30183: [VAULT-35046] Show namespace count label
Author: login: beagins
Labels: name: hashicorp-contributed-pr
description: If the PR is HashiCorp (i.e. not-community) contributed
Created: 2025-04-04T00:37:41Z | Merged: 2025-04-18T01:03:40Z

### Description
| **This branch is merging into a feature side branch** (not `main`) |
| --- |

| The current implementation looks ugly, styling will be addressed in a separate PR** |
| --- |
- [X] Replace `Hds::SuperSelect` with `Hds::Dropdown` component*
- [X] Show namespace count label: All/Matching Namespaces (#)
- [X] Enterprise tests passing ✅ 
  > 236 tests completed in 174031 milliseconds, with 0 failed, 10 skipped, and 0 todo.
  > 1433 assertions of 1433 passed, 0 failed.

\* The `SuperSelect` component was limited in regards to implementing search across an array of objects instead of strings, and when displaying the namespace count label below the search field. The `Dropdown` component is more flexible, making it possible for the implementation to match the existing design.

### Screenshots
![Screenshot 2025-04-16 at 12 49 32 PM](https://github.com/user-attachments/assets/3fe0da8c-85ef-431d-964e-84a4d31eb583)

### TODO only if you're a HashiCorp employee
- [ ] **Backport Labels:** If this fix needs to be backported, use the appropriate `backport/` label that matches the desired release branch. Note that in the CE repo, the latest release branch will look like `backport/x.x.x`, but older release branches will be `backport/ent/x.x.x+ent`.
    - [ ] **LTS**: If this fixes a critical security vulnerability or [severity 1](https://www.hashicorp.com/customer-success/enterprise-support) bug, it will also need to be backported to the current [LTS versions](https://developer.hashicorp.com/vault/docs/enterprise/lts#why-is-there-a-risk-to-updating-to-a-non-lts-vault-enterprise-version) of Vault. To ensure this, use **all** available enterprise labels.
- [ ] **ENT Breakage:** If this PR either 1) removes a public function OR 2) changes the signature
  of a public function, even if that change is in a CE file, _double check_ that
  applying the patch for this PR to the ENT repo and running tests doesn't
  break any tests. Sometimes ENT only tests rely on public functions in CE
  files.
- [ ] **Jira:** If this change has an associated Jira, it's referenced either
  in the PR description, commit message, or branch name.
- [ ] **RFC:** If this change has an associated RFC, please link it in the description.
- [ ] **ENT PR:** If this change has an associated ENT PR, please link it in the
  description. Also, make sure the changelog is in this PR, _not_ in your ENT PR.



Diff:
diff --git a/ui/app/components/namespace-picker.hbs b/ui/app/components/namespace-picker.hbs
index c7b0f907abf..41a998133a1 100644
--- a/ui/app/components/namespace-picker.hbs
+++ b/ui/app/components/namespace-picker.hbs
@@ -1,23 +1,35 @@
 {{!
-Copyright (c) HashiCorp, Inc.
-SPDX-License-Identifier: BUSL-1.1
+  Copyright (c) HashiCorp, Inc.
+  SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{or this.selected.id "-"}} data-test-namespace-toggle />
+
+    <D.Generic>
+      {{this.namespaceLabel}}
+      <Hds::BadgeCount @text={{or this.options.length 0}} />
+    </D.Generic>
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{eq option.id this.selected.id}}
+        {{on "click" (fn this.onChange option)}}
+        data-test-namespace-link={{option.path}}
+      >
+        {{option.label}}
+      </D.Checkmark>
+    {{/each}}
+
+    {{! Check if the user has permissions to list namespaces. If true, display additional footer options like "Refresh list" and "Manage". }}
+    {{#if this.hasListPermissions}}
+      <D.Footer @hasDivider={{true}}>
+        <Hds::Button @color="secondary" @text="Refresh list" {{on "click" this.loadOptions}} data-test-refresh-namespaces />
+        <Hds::Button @color="secondary" @text="Manage" @icon="settings" @route="vault.cluster.access.namespaces" />
+      </D.Footer>
+    {{/if}}
+
+  </Hds::Dropdown>
 </div>
\ No newline at end of file
diff --git a/ui/app/components/namespace-picker.js b/ui/app/components/namespace-picker.js
index 64a0c565c4a..db473709357 100644
--- a/ui/app/components/namespace-picker.js
+++ b/ui/app/components/namespace-picker.js
@@ -25,10 +25,11 @@ export default class NamespacePicker extends Component {
   @service store;
 
   // Show/hide refresh & manage namespaces buttons
-  @tracked showAfterOptions = false;
+  @tracked hasListPermissions = false;
 
   @tracked selected = {};
   @tracked options = [];
+  @tracked searchInput = '';
 
   constructor() {
     super(...arguments);
@@ -72,6 +73,10 @@ export default class NamespacePicker extends Component {
     ];
   }
 
+  get namespaceLabel() {
+    return this.searchInput === '' ? 'All namespaces' : 'Matching namespaces';
+  }
+
   @action
   async fetchListCapability() {
     // TODO: Revist. This logic was carried over from previous component implmenetation.
@@ -79,11 +84,11 @@ export default class NamespacePicker extends Component {
     //  instead of hiding both the "Manage" and "Refresh List" buttons?
     try {
       await this.store.findRecord('capabilities', 'sys/namespaces/');
-      this.showAfterOptions = true;
+      this.hasListPermissions = true;
     } catch (e) {
       // If error out on findRecord call it's because you don't have permissions
       // and therefore don't have permission to manage namespaces
-      this.showAfterOptions = false;
+      this.hasListPermissions = false;
     }
   }
 
diff --git a/ui/app/components/namespace-picker/after-options.hbs b/ui/app/components/namespace-picker/after-options.hbs
deleted file mode 100644
index 85bce792004..00000000000
--- a/ui/app/components/namespace-picker/after-options.hbs
+++ /dev/null
@@ -1,13 +0,0 @@
-{{!
-  Copyright (c) HashiCorp, Inc.
-  SPDX-License-Identifier: BUSL-1.1
-}}
-<div>
-  <Hds::Button
-    @color="secondary"
-    @text="Refresh List"
-    {{on "click" (action "refreshNamespaceList")}}
-    data-test-refresh-namespaces
-  />
-  <Hds::Button @color="secondary" @text="Manage" @icon="settings" @route="vault.cluster.access.namespaces" />
-</div>
\ No newline at end of file
diff --git a/ui/app/components/namespace-picker/after-options.ts b/ui/app/components/namespace-picker/after-options.ts
deleted file mode 100644
index ce059cb0710..00000000000
--- a/ui/app/components/namespace-picker/after-options.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-/**
- * Copyright (c) HashiCorp, Inc.
- * SPDX-License-Identifier: BUSL-1.1
- */
-
-import Component from '@glimmer/component';
-import { action } from '@ember/object';
-
-interface AfterOptionsArgs {
-  loadOptions: () => void;
-}
-
-/**
- * @module AfterOptions
- * @description component is used to display action items inside the namespace picker dropdown.
- *  The "Manage" button directs the user to the namespace management page.
- *  The "Refresh List" button refrehes the list of namespaces in the dropdown.
- *
- * @example
- * @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
- */
-
-export default class AfterOptions extends Component<AfterOptionsArgs> {
-  @action
-  refreshNamespaceList() {
-    this.args?.loadOptions();
-  }
-}
diff --git a/ui/app/components/namespace-picker/selected-option.hbs b/ui/app/components/namespace-picker/selected-option.hbs
deleted file mode 100644
index ba139800c73..00000000000
--- a/ui/app/components/namespace-picker/selected-option.hbs
+++ /dev/null
@@ -1,11 +0,0 @@
-{{!
-  Copyright (c) HashiCorp, Inc.
-  SPDX-License-Identifier: BUSL-1.1
-}}
-
-<Hds::Text::Body>
-  <div class="is-flex-center is-flex-1">
-    <Icon @name="org" />
-    <span data-test-selected-namespace="{{@option.label}}">{{@option.id}}</span>
-  </div>
-</Hds::Text::Body>
\ No newline at end of file
diff --git a/ui/tests/acceptance/enterprise-namespaces-test.js b/ui/tests/acceptance/enterprise-namespaces-test.js
index 9e75e8bcef1..decd4f4d8ff 100644
--- a/ui/tests/acceptance/enterprise-namespaces-test.js
+++ b/ui/tests/acceptance/enterprise-namespaces-test.js
@@ -9,6 +9,8 @@ import { setupApplicationTest } from 'ember-qunit';
 import { runCmd, createNS } from 'vault/tests/helpers/commands';
 import authPage from 'vault/tests/pages/auth';
 import logout from 'vault/tests/pages/logout';
+import { GENERAL } from '../helpers/general-selectors';
+import { NAMESPACE_PICKER_SELECTORS } from '../helpers/namespace-picker';
 
 module('Acceptance | Enterprise | namespaces', function (hooks) {
   setupApplicationTest(hooks);
@@ -23,15 +25,18 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
     const token = await runCmd(`write -field=client_token auth/token/create policies=default`);
     await logout.visit();
     await authPage.login(token);
-    await click('[data-test-namespace-toggle]');
-    assert.dom('[aria-selected="true"]').hasText('root', 'root renders as current namespace');
-    assert.dom('[data-option-index]').exists({ count: 1 }, 'Only the root namespace exists');
+    await click(NAMESPACE_PICKER_SELECTORS.toggle);
+    assert.dom(NAMESPACE_PICKER_SELECTORS.link()).hasText('root', 'root renders as current namespace');
+    assert
+      .dom(`${NAMESPACE_PICKER_SELECTORS.link()} svg${GENERAL.icon('check')}`)
+      .exists('The root namespace is selected');
   });
 
+  // TODO: Is this test description still accurate?
   test('it shows nested namespaces if you log in with a namespace starting with a /', async function (assert) {
-    assert.expect(5);
+    assert.expect(6);
 
-    await click('[data-test-namespace-toggle]');
+    await click(NAMESPACE_PICKER_SELECTORS.toggle);
 
     const nses = ['beep', 'boop', 'bop'];
     for (const [i, ns] of nses.entries()) {
@@ -41,12 +46,12 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
       const targetNamespace = nses.slice(0, i + 1).join('/');
       const url = `/vault/secrets?namespace=${targetNamespace}`;
       // this is usually triggered when creating a ns in the form -- trigger a reload of the namespaces manually
-      await click('[data-test-namespace-toggle]');
-      await click('[data-test-refresh-namespaces]');
-      await waitFor(`[data-option-index="${i + 1}"]`);
+      await click(NAMESPACE_PICKER_SELECTORS.toggle);
+      await click(NAMESPACE_PICKER_SELECTORS.refreshList);
+      await waitFor(NAMESPACE_PICKER_SELECTORS.link(targetNamespace));
       // check that the full namespace path, like "beep/boop", shows in the toggle display
       assert
-        .dom(`[data-option-index="${i + 1}"]`)
+        .dom(NAMESPACE_PICKER_SELECTORS.link(targetNamespace))
         .hasText(targetNamespace, `shows the namespace ${targetNamespace} in the toggle component`);
       // because quint does not like page reloads, visiting url directly instead of clicking on namespace in toggle
       await visit(url);
@@ -58,14 +63,27 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
     await settled();
     await authPage.tokenInput('root').submit();
     await settled();
-    await click('[data-test-namespace-toggle]');
-    await waitFor('[aria-selected="true"]');
-    assert
-      .dom('[aria-selected="true"]')
-      .hasText('beep/boop', 'current namespace does not begin or end with /');
-    assert
-      .dom('[data-option-index="3"]')
-      .hasText('beep/boop/bop', 'shows the full namespace path in the toggle');
+
+    // Open the namespace picker & wait for it to render
+    await click(NAMESPACE_PICKER_SELECTORS.toggle);
+    await waitFor(`svg${GENERAL.icon('check')}`);
+
+    // Find the selected element with the check icon & ensure it exists
+    const checkIcon = document.querySelector(
+      `${NAMESPACE_PICKER_SELECTORS.link()} svg${GENERAL.icon('check')}`
+    );
+    assert.ok(checkIcon, 'A selected namespace link with the check icon exists');
+
+    // Get the selected namespace with the data-test-namespace-link attribute & ensure it exists
+    const selectedNamespace = checkIcon.closest(NAMESPACE_PICKER_SELECTORS.link());
+    assert.ok(selectedNamespace, 'The selected namespace link exists');
+
+    // Verify that the selected namespace has the correct data-test-namespace-link attribute and path value
+    assert.strictEqual(
+      selectedNamespace.getAttribute('data-test-namespace-link'),
+      'beep/boop',
+      'The current namespace does not begin or end with /'
+    );
   });
 
   test('it shows the regular namespace toolbar when not managed', async function (assert) {
diff --git a/ui/tests/helpers/namespace-picker.js b/ui/tests/helpers/namespace-picker.js
new file mode 100644
index 00000000000..dca2b68988f
--- /dev/null
+++ b/ui/tests/helpers/namespace-picker.js
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+export const NAMESPACE_PICKER_SELECTORS = {
+  link: (link) => (link ? `[data-test-namespace-link="${link}"]` : '[data-test-namespace-link]'),
+  refreshList: '[data-test-refresh-namespaces]',
+  toggle: '[data-test-namespace-toggle]',
+};


Reviews:
user: login: hellobontempo
body: Nice job! I know fiddling with a component for a while and abandoning it can be a rough journey. Now you'll be the SuperSelect expert on when to and not to use 😂 

Most of the comments are just some funny ember things, just a couple questions about implementation. Looks great, though! 😄 
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T02:03:58Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:45:03Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:46:11Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:48:14Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:48:57Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:51:17Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:54:09Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:56:34Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:59:38Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T17:01:51Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T17:03:14Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T17:04:08Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T16:13:32Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T16:13:51Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T16:15:52Z
user: login: Monkeychip
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:16:03Z
user: login: Monkeychip
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:20:45Z
user: login: hellobontempo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:21:02Z
user: login: hellobontempo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:22:03Z
user: login: hellobontempo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:22:12Z
user: login: Monkeychip
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:22:17Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:25:41Z
user: login: hellobontempo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:26:20Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:28:54Z
user: login: hellobontempo
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T17:30:35Z
user: login: hellobontempo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-17T19:11:12Z

Review comments:
diff_hunk: @@ -30,6 +30,9 @@ export default class NamespacePicker extends Component {
   @tracked selected = {};
   @tracked options = [];
 
+  @tracked isSelected = (option) => this.selected?.id === option?.id;
+  @tracked selectedNamespace = () => this.selected?.id || '-';
path: ui/app/components/namespace-picker.js
user: login: hellobontempo
body: is this just to return the id for the selected namespace? A getter would be more appropriate to use here, if so. Local helper functions are great for passing args that we don't have easy access to in the component file (like if we're iterating in an `{{#each}}` loop)


created_at: 2025-04-16T01:28:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
+        {{on "click" (action "onChange" option)}}
+        data-test-namespace-link={{option.path}}
+        data-test-current-namespace={{this.isSelected option}}
+      >
+        {{option.label}}
+      </D.Checkmark>
+    {{/each}}
+
+    <D.Footer @hasDivider={{true}}>
+      <Hds::Button
+        @color="secondary"
+        @text="Refresh List"
+        {{on "click" (action "loadOptions")}}
path: ui/app/components/namespace-picker.hbs
user: login: hellobontempo
body: same comment about using "action"
created_at: 2025-04-16T01:29:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
+        {{on "click" (action "onChange" option)}}
+        data-test-namespace-link={{option.path}}
+        data-test-current-namespace={{this.isSelected option}}
path: ui/app/components/namespace-picker.hbs
user: login: hellobontempo
body: do we need both of these test selectors? it's usually better to assert an item is selected by checking the class or that the icon is present for that item instead of bloat the template with test selectors. That way we're also testing the DOM display is what we expect and not just testing a test selector is receiving the value we want.
created_at: 2025-04-16T01:30:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
+        {{on "click" (action "onChange" option)}}
path: ui/app/components/namespace-picker.hbs
user: login: hellobontempo
body: the `action` helper is outdated syntax (from classic ember days), in ember octane it should either be `this.myFunction` or `fn this.myFunction "some arg"` if you're passing an arg 

https://ember-learn.github.io/ember-octane-vs-classic-cheat-sheet/
created_at: 2025-04-16T01:32:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +30,9 @@ export default class NamespacePicker extends Component {
   @tracked selected = {};
   @tracked options = [];
 
+  @tracked isSelected = (option) => this.selected?.id === option?.id;
path: ui/app/components/namespace-picker.js
user: login: hellobontempo
body: I've never seen `@tracked` functions 🤔 are these [local helper functions](https://guides.emberjs.com/release/components/helper-functions/#toc_local-helper-functions)? We shouldn't need the tracked decorator here, if so.
created_at: 2025-04-16T01:34:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
+        {{on "click" (action "onChange" option)}}
+        data-test-namespace-link={{option.path}}
+        data-test-current-namespace={{this.isSelected option}}
+      >
+        {{option.label}}
+      </D.Checkmark>
+    {{/each}}
+
+    <D.Footer @hasDivider={{true}}>
+      <Hds::Button
+        @color="secondary"
+        @text="Refresh List"
path: ui/app/components/namespace-picker.hbs
user: login: hellobontempo
body: definitely a design question/nit, but I buttons are typically sentenced cased (not titlecased) throughout the codebase
created_at: 2025-04-16T01:37:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
path: ui/app/components/namespace-picker.hbs
user: login: hellobontempo
body: using a helper function is totally fine for this, often for checking if an item is selected it's simple enough logic we can do it inline like: `{{eq option.id this.selected.id}}`

it's definitely potato potato 🥔  - whatever feels easier to read. Personally, I find simple template logic is nice in line so there's less jumping back and forth to the component logic. I then move it to the component as it gets more complex.
created_at: 2025-04-16T01:39:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
+        {{on "click" (action "onChange" option)}}
+        data-test-namespace-link={{option.path}}
+        data-test-current-namespace={{this.isSelected option}}
+      >
+        {{option.label}}
+      </D.Checkmark>
+    {{/each}}
+
+    <D.Footer @hasDivider={{true}}>
+      <Hds::Button
+        @color="secondary"
+        @text="Refresh List"
+        {{on "click" (action "loadOptions")}}
+        data-test-refresh-namespaces
+      />
+      <Hds::Button @color="secondary" @text="Manage" @icon="settings" @route="vault.cluster.access.namespaces" />
path: ui/app/components/namespace-picker.hbs
user: login: hellobontempo
body: I think we've discussed this, but we'll want to wrap this in permissions at some point. That's fine to do in a follow-on but perhaps we should add a todo comment so it's not forgotten?
created_at: 2025-04-16T01:41:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+{{!
+  Copyright (c) HashiCorp, Inc.
+  SPDX-License-Identifier: BUSL-1.1
+}}
+<div>
+  <span>{{this.scope}} Namespaces </span>
path: ui/app/components/namespace-picker/title.hbs
user: login: hellobontempo
body: nit: this seems like it should also be sentenced cased, but I'll defer to design. Also, is the extra space necessary? Wondering if we should give the badge a little padding, instead  🤔 
```suggestion
  <span>{{this.scope}} Namespaces</span>
```
created_at: 2025-04-16T01:42:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+{{!
+  Copyright (c) HashiCorp, Inc.
+  SPDX-License-Identifier: BUSL-1.1
+}}
+<div>
+  <span>{{this.scope}} Namespaces </span>
+  <Hds::Badge @text={{this.count}} @value={{this.count}} />
path: ui/app/components/namespace-picker/title.hbs
user: login: hellobontempo
body: Since we're displaying numbers I think using [Hds::BadgeCount](https://helios.hashicorp.design/components/badge-count) is more appropriate here. I also don't see a `@value` arg and think that can be removed 😄 
created_at: 2025-04-16T01:46:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+
+interface TitleArgs {
+  all: boolean;
+  count: () => void;
+}
+
+/**
+ * @module Title
+ * @description Component used to display title in the namespace-picker.
+ * By default, display "All Namespaces" with the total count.
+ * When a search is entered, display "Matching Namespaces" with the count of matching results.
+ *
+ * @example
+ * {{component "namespace-picker/title" count=this.options.count}}
+ */
+
+export default class Title extends Component<TitleArgs> {
path: ui/app/components/namespace-picker/title.ts
user: login: hellobontempo
body: We don't need this backing class and can just render `@count` and `@scope` in the template directly. They will update automatically - dug up the [docs](https://guides.emberjs.com/v4.6.0/in-depth-topics/autotracking-in-depth/) for you! 😄 🕵️ 

<img width="600" alt="Screenshot 2025-04-15 at 6 54 05 PM" src="https://github.com/user-attachments/assets/5efb231f-9182-4901-b593-e944ad45111e" />

created_at: 2025-04-16T01:54:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+
+interface TitleArgs {
+  all: boolean;
path: ui/app/components/namespace-picker/title.ts
user: login: hellobontempo
body: I don't see this passed to the component? It looks like `@scope` is passed directly
created_at: 2025-04-16T01:56:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
path: ui/app/components/namespace-picker.hbs
user: login: hellobontempo
body: Just out of curiosity was using ` <D.Generic>` weird stylistically? It seems a little more straightforward to render the title/count directly instead of passing it as a `@text` arg (which wow - I'm impressed you figured that out! 😂 🙌  )

Especially since the title component is so simple separating it out seems a little verbose and keeping it all here would read a little easier.
created_at: 2025-04-16T02:02:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
path: ui/app/components/namespace-picker.hbs
user: login: beagins
body: I'll try out `<D.Generic>` and see - I was going based off of the HDS code example, which used `Title` but `Generic` is probably more straightforward if it allows me to get rid of the `Title` component. Thanks for calling this out!!
created_at: 2025-04-16T16:45:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
path: ui/app/components/namespace-picker.hbs
user: login: beagins
body: got it, that makes sense! yea it's nice to not switch back and forth between files when trying to understand how a component works, especially for a quick check like this one! I'll update this
created_at: 2025-04-16T16:46:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
+        {{on "click" (action "onChange" option)}}
path: ui/app/components/namespace-picker.hbs
user: login: beagins
body: thank you for linking the cheat sheet! this is SUPER helpful!! I've been having a hard time with outdated vs current ember patterns, and this is SO helpful! I'll go ahead and update this. Thank you!!
created_at: 2025-04-16T16:48:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
+        {{on "click" (action "onChange" option)}}
+        data-test-namespace-link={{option.path}}
+        data-test-current-namespace={{this.isSelected option}}
path: ui/app/components/namespace-picker.hbs
user: login: beagins
body: that makes sense - this was just the path of least resistance when updating the failing tests, I'll go ahead and update the test to check for the checkmark instead. good catch, thank you!!
created_at: 2025-04-16T16:48:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
+        {{on "click" (action "onChange" option)}}
+        data-test-namespace-link={{option.path}}
+        data-test-current-namespace={{this.isSelected option}}
+      >
+        {{option.label}}
+      </D.Checkmark>
+    {{/each}}
+
+    <D.Footer @hasDivider={{true}}>
+      <Hds::Button
+        @color="secondary"
+        @text="Refresh List"
path: ui/app/components/namespace-picker.hbs
user: login: beagins
body: thank you for catching this! the design is definitely sentenced cased - the previous app I worked on was all titlecased, so I'll be sure to pay more attention to this. old habits die hard!! thank you!!
created_at: 2025-04-16T16:51:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
+        {{on "click" (action "onChange" option)}}
+        data-test-namespace-link={{option.path}}
+        data-test-current-namespace={{this.isSelected option}}
+      >
+        {{option.label}}
+      </D.Checkmark>
+    {{/each}}
+
+    <D.Footer @hasDivider={{true}}>
+      <Hds::Button
+        @color="secondary"
+        @text="Refresh List"
+        {{on "click" (action "loadOptions")}}
+        data-test-refresh-namespaces
+      />
+      <Hds::Button @color="secondary" @text="Manage" @icon="settings" @route="vault.cluster.access.namespaces" />
path: ui/app/components/namespace-picker.hbs
user: login: beagins
body: good catch! I had implemented the permissions check when the component was using `SuperSelect` (with `showAfterOptions`), but it got lost in the shuffle to using `Dropdown`. I'll add a comment to ensure it is addressed in another pr. Thanks Claire!!
created_at: 2025-04-16T16:54:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +30,9 @@ export default class NamespacePicker extends Component {
   @tracked selected = {};
   @tracked options = [];
 
+  @tracked isSelected = (option) => this.selected?.id === option?.id;
path: ui/app/components/namespace-picker.js
user: login: beagins
body: I think this fn can be replaced w/ the inline `eq` fn you mentioned in a comment above. But now I'm curious why I added `@tracked` here - I don't think I would have added it if it was working without it. Out of curiosity I'll play around with it and report back :) 
created_at: 2025-04-16T16:56:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +30,9 @@ export default class NamespacePicker extends Component {
   @tracked selected = {};
   @tracked options = [];
 
+  @tracked isSelected = (option) => this.selected?.id === option?.id;
+  @tracked selectedNamespace = () => this.selected?.id || '-';
path: ui/app/components/namespace-picker.js
user: login: beagins
body: yes, something was breaking on the initial render when `this.selected` is still `undefined`. Adding optional chaining inside the template didn't resolve it - I can't remember exactly why, but I can double check, it may have been from when I was using `SuperSelect`. If that's still the case, a getter definitely makes sense. I'll update it. Thanks!!
created_at: 2025-04-16T16:59:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+{{!
+  Copyright (c) HashiCorp, Inc.
+  SPDX-License-Identifier: BUSL-1.1
+}}
+<div>
+  <span>{{this.scope}} Namespaces </span>
path: ui/app/components/namespace-picker/title.hbs
user: login: beagins
body: good point - padding is a lot more intentional and less likely to get removed. this was probably just a quick thing I did to avoid dropping down into the styling rabbit hole for now, but it'd be easy to forget to come back and fix it later. I'll go ahead and update this. thank you!!
created_at: 2025-04-16T17:01:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,8 @@
+{{!
+  Copyright (c) HashiCorp, Inc.
+  SPDX-License-Identifier: BUSL-1.1
+}}
+<div>
+  <span>{{this.scope}} Namespaces </span>
+  <Hds::Badge @text={{this.count}} @value={{this.count}} />
path: ui/app/components/namespace-picker/title.hbs
user: login: beagins
body: omgosh I didn't see that there's a specific numeric badge component - thank you!! I think I added `text` and `value` because both were required fields for the `Badge` component (omitting one resulted in console errors). But as long as that isn't the case for `BadgeCount`, I'll go ahead and remove `@value`, thanks!!
created_at: 2025-04-16T17:03:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,27 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { tracked } from '@glimmer/tracking';
+
+interface TitleArgs {
+  all: boolean;
+  count: () => void;
+}
+
+/**
+ * @module Title
+ * @description Component used to display title in the namespace-picker.
+ * By default, display "All Namespaces" with the total count.
+ * When a search is entered, display "Matching Namespaces" with the count of matching results.
+ *
+ * @example
+ * {{component "namespace-picker/title" count=this.options.count}}
+ */
+
+export default class Title extends Component<TitleArgs> {
path: ui/app/components/namespace-picker/title.ts
user: login: beagins
body: thank you!!!!
created_at: 2025-04-16T17:04:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +30,9 @@ export default class NamespacePicker extends Component {
   @tracked selected = {};
   @tracked options = [];
 
+  @tracked isSelected = (option) => this.selected?.id === option?.id;
path: ui/app/components/namespace-picker.js
user: login: beagins
body: PS - both isSelected and selectedNamespace are removed as of the last commit, but did confirm that removing @tracked it didn't break/change anything. These must have started off as tracked variables and somewhere along the line I switched them but didn't remove @tracked. Thanks for catching this Claire! :)
created_at: 2025-04-17T16:13:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,6 +30,9 @@ export default class NamespacePicker extends Component {
   @tracked selected = {};
   @tracked options = [];
 
+  @tracked isSelected = (option) => this.selected?.id === option?.id;
+  @tracked selectedNamespace = () => this.selected?.id || '-';
path: ui/app/components/namespace-picker.js
user: login: beagins
body: both of these helper functions are removed in the latest commit. 
created_at: 2025-04-17T16:13:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,35 @@
 {{!
-Copyright (c) HashiCorp, Inc.
-SPDX-License-Identifier: BUSL-1.1
+  Copyright (c) HashiCorp, Inc.
+  SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{(or this.selected.id "-")}} data-test-namespace-toggle />
+
+    <D.Generic>
+      {{this.namespaceLabel}}
+      <Hds::BadgeCount @text={{(or this.options.length 0)}} />
+    </D.Generic>
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{eq option.id this.selected.id}}
+        {{on "click" (fn this.onChange option)}}
+        data-test-namespace-link={{option.path}}
+      >
+        {{option.label}}
+      </D.Checkmark>
+    {{/each}}
+
+    {{! Check if the user has permissions to list namespaces. If true, display additional footer options like "Refresh list" and "Manage". }}
+    {{#if this.hasListPermissions}}
path: ui/app/components/namespace-picker.hbs
user: login: beagins
body: Note: The previous implementation of namespace picker hid both the Refresh & Manage buttons if the user did not have permissions to list namespaces. I kept this logic for now, but added a TODO in the component file to see if it makes sense to hide both buttons or only hide the Manage button.
created_at: 2025-04-17T16:15:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,10 +25,12 @@ export default class NamespacePicker extends Component {
   @service store;
 
   // Show/hide refresh & manage namespaces buttons
-  @tracked showAfterOptions = false;
+  @tracked hasListPermissions = false;
 
   @tracked selected = {};
   @tracked options = [];
+  @tracked search = '';
+  @tracked namespaceLabel = 'All Namespaces';
path: ui/app/components/namespace-picker.js
user: login: Monkeychip
body: If the title does not change it does not need to be tracked. If there is only one instance of it, hardcode it in the hbs file, otherwise you can assign it here as: namespaceLabel = 'All Namespaces'. Tracked properties are helpful when we expect them to be changed based of of something happening either to arguments passed in or a user action. 
AI overview below:
>In Ember.js, you should use @tracked properties for any property that is mutated and potentially externally visible. This includes properties that are used in templates or computed properties, or that might be accessed or updated by other components or services. Essentially, if a property's change needs to be reflected in the UI or in other parts of your application, it should be tracked. 
created_at: 2025-04-17T17:16:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,14 +60,25 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
     await settled();
     await authPage.tokenInput('root').submit();
     await settled();
+
+    // Open the namespace picker & wait for it to render
     await click('[data-test-namespace-toggle]');
-    await waitFor('[aria-selected="true"]');
-    assert
-      .dom('[aria-selected="true"]')
-      .hasText('beep/boop', 'current namespace does not begin or end with /');
-    assert
-      .dom('[data-option-index="3"]')
-      .hasText('beep/boop/bop', 'shows the full namespace path in the toggle');
+    await waitFor('svg[data-test-icon="check"]');
path: ui/tests/acceptance/enterprise-namespaces-test.js
user: login: Monkeychip
body: A pattern we're moving towards with data-test selectors, especially those that are commonly used, is to import them from `tests/helpers`. See general-selectors [here](https://github.com/hashicorp/vault/blob/main/ui/tests/helpers/general-selectors.ts#L15) for the icon one and it being used [here](https://github.com/hashicorp/vault/blob/main/ui/tests/integration/components/tools/unwrap-test.js#L85).

A good rule of thumb is if you're hard-coding data-test-whatever into a test file, search test `tests/helpers` directory to see if it's already been defined.
created_at: 2025-04-17T17:20:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,35 @@
 {{!
-Copyright (c) HashiCorp, Inc.
-SPDX-License-Identifier: BUSL-1.1
+  Copyright (c) HashiCorp, Inc.
+  SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{(or this.selected.id "-")}} data-test-namespace-toggle />
+
+    <D.Generic>
+      {{this.namespaceLabel}}
+      <Hds::BadgeCount @text={{(or this.options.length 0)}} />
+    </D.Generic>
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{eq option.id this.selected.id}}
+        {{on "click" (fn this.onChange option)}}
+        data-test-namespace-link={{option.path}}
+      >
+        {{option.label}}
+      </D.Checkmark>
+    {{/each}}
+
+    {{! Check if the user has permissions to list namespaces. If true, display additional footer options like "Refresh list" and "Manage". }}
+    {{#if this.hasListPermissions}}
path: ui/app/components/namespace-picker.hbs
user: login: hellobontempo
body: I _think_ the list one is un-gated, so everyone has access (it will just only return what a user can view) The permissions wrap should only apply to hide/showing `manage` 

Just an FYI 😄 but definitely fine to address as a follow-on
created_at: 2025-04-17T17:21:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,35 @@
 {{!
-Copyright (c) HashiCorp, Inc.
-SPDX-License-Identifier: BUSL-1.1
+  Copyright (c) HashiCorp, Inc.
+  SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{(or this.selected.id "-")}} data-test-namespace-toggle />
path: ui/app/components/namespace-picker.hbs
user: login: hellobontempo
body: just fyi, the additional parenthesis aren't necessary here since the conditional is at the "top" level

```suggestion
    <D.ToggleButton @icon="org" @text={{or this.selected.id "-"}} data-test-namespace-toggle />
```
created_at: 2025-04-17T17:22:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,23 +1,35 @@
 {{!
-Copyright (c) HashiCorp, Inc.
-SPDX-License-Identifier: BUSL-1.1
+  Copyright (c) HashiCorp, Inc.
+  SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{(or this.selected.id "-")}} data-test-namespace-toggle />
+
+    <D.Generic>
+      {{this.namespaceLabel}}
+      <Hds::BadgeCount @text={{(or this.options.length 0)}} />
path: ui/app/components/namespace-picker.hbs
user: login: hellobontempo
body: ```suggestion
      <Hds::BadgeCount @text={{or this.options.length 0}} />
```
created_at: 2025-04-17T17:22:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,14 +60,25 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
     await settled();
     await authPage.tokenInput('root').submit();
     await settled();
+
+    // Open the namespace picker & wait for it to render
     await click('[data-test-namespace-toggle]');
-    await waitFor('[aria-selected="true"]');
-    assert
-      .dom('[aria-selected="true"]')
-      .hasText('beep/boop', 'current namespace does not begin or end with /');
-    assert
-      .dom('[data-option-index="3"]')
-      .hasText('beep/boop/bop', 'shows the full namespace path in the toggle');
+    await waitFor('svg[data-test-icon="check"]');
path: ui/tests/acceptance/enterprise-namespaces-test.js
user: login: Monkeychip
body: Also, feel free to add selectors to these files as well. We have the general-selectors but also component or engine specific helpers in the helpers/ directory. Not sure if one is needed for namespaces specifically, I'll leave that one to you.
created_at: 2025-04-17T17:22:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,10 +25,12 @@ export default class NamespacePicker extends Component {
   @service store;
 
   // Show/hide refresh & manage namespaces buttons
-  @tracked showAfterOptions = false;
+  @tracked hasListPermissions = false;
 
   @tracked selected = {};
   @tracked options = [];
+  @tracked search = '';
+  @tracked namespaceLabel = 'All Namespaces';
path: ui/app/components/namespace-picker.js
user: login: beagins
body: it will change - in my next PR i'm implementing search and it will switch between "All namespaces" and "Matching namespaces" based on whether or not the user has typed anything in the search field. 
created_at: 2025-04-17T17:25:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,10 +25,12 @@ export default class NamespacePicker extends Component {
   @service store;
 
   // Show/hide refresh & manage namespaces buttons
-  @tracked showAfterOptions = false;
+  @tracked hasListPermissions = false;
 
   @tracked selected = {};
   @tracked options = [];
+  @tracked search = '';
+  @tracked namespaceLabel = 'All Namespaces';
path: ui/app/components/namespace-picker.js
user: login: hellobontempo
body: I think the functionality here is going to be added in a follow-on PR because this will updated to be "All namespace" or "Matching namespaces"

Shannon - when the time comes I think this would make sense as a getter that updates dynamically if/when a filter is applied

something like the following (`searchInput` is just a placeholder, whatever you decide or have in mind works)

```js
get namespaceLabel() {

return this.search === '' : "All namespaces" : "Matching namespaces" 
}
created_at: 2025-04-17T17:26:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -58,14 +60,25 @@ module('Acceptance | Enterprise | namespaces', function (hooks) {
     await settled();
     await authPage.tokenInput('root').submit();
     await settled();
+
+    // Open the namespace picker & wait for it to render
     await click('[data-test-namespace-toggle]');
-    await waitFor('[aria-selected="true"]');
-    assert
-      .dom('[aria-selected="true"]')
-      .hasText('beep/boop', 'current namespace does not begin or end with /');
-    assert
-      .dom('[data-option-index="3"]')
-      .hasText('beep/boop/bop', 'shows the full namespace path in the toggle');
+    await waitFor('svg[data-test-icon="check"]');
path: ui/tests/acceptance/enterprise-namespaces-test.js
user: login: beagins
body: Oooo nice! Thank you for linking examples, this makes a lot of sense :) I'll update accordingly
created_at: 2025-04-17T17:28:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -4,20 +4,32 @@ SPDX-License-Identifier: BUSL-1.1
 }}
 
 <div class="namespace-picker" ...attributes>
-  <Hds::Form::SuperSelect::Single::Field
-    @afterOptionsComponent={{component "namespace-picker/after-options" loadOptions=this.loadOptions}}
-    @ariaLabel="Namespace"
-    @onChange={{this.onChange}}
-    @options={{this.options}}
-    @placeholder="Search"
-    @searchEnabled={{true}}
-    @selected={{this.selected}}
-    @selectedItemComponent={{component "namespace-picker/selected-option"}}
-    @showAfterOptions={{this.showAfterOptions}}
-    @verticalPosition="above"
-    data-test-namespace-toggle
-    as |F|
-  >
-    <F.Options>{{F.options.label}}</F.Options>
-  </Hds::Form::SuperSelect::Single::Field>
+  <Hds::Dropdown @enableCollisionDetection={{true}} as |D|>
+
+    <D.ToggleButton @icon="org" @text={{this.selectedNamespace}} data-test-namespace-toggle />
+
+    <D.Title @text={{component "namespace-picker/title" count=this.options.length}} />
+
+    {{#each this.options as |option|}}
+      <D.Checkmark
+        @selected={{this.isSelected option}}
+        {{on "click" (action "onChange" option)}}
+        data-test-namespace-link={{option.path}}
+        data-test-current-namespace={{this.isSelected option}}
+      >
+        {{option.label}}
+      </D.Checkmark>
+    {{/each}}
+
+    <D.Footer @hasDivider={{true}}>
+      <Hds::Button
+        @color="secondary"
+        @text="Refresh List"
path: ui/app/components/namespace-picker.hbs
user: login: hellobontempo
body: We also haven't been the best about being consistent but have recently made an effort to be, so always feel free to ask if there's not a clear pattern one way or another!
created_at: 2025-04-17T19:11:12Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-04T01:01:13Z
body: CI Results:
All Go tests succeeded! :white_check_mark:
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-16T00:00:38Z
body: Build Results:
All builds succeeded! :white_check_mark:
author_association: NONE