Repository: hashicorp/vault
File: ui/app/components/namespace-picker.ts (TypeScript)

Code before:

  @tracked canManageNamespaces = false; // Show/hide manage namespaces button
  @tracked canRefreshNamespaces = false; // Show/hide refresh list button
  @tracked errorLoadingNamespaces = '';
  @tracked hasNamespaces = false;
  @tracked searchInput = '';
  @tracked searchInputHelpText =
    "Enter a full path in the search bar and hit the 'Enter' ↵ key to navigate faster.";

  constructor(owner: unknown, args: Record<string, never>) {
    super(owner, args);
    this.loadOptions();
  }

  get allNamespaces(): NamespaceOption[] {
    return this.getOptions(
      this.namespace?.accessibleNamespaces,
      this.namespace?.currentNamespace,
      this.namespace?.path
    );
  }

  get selectedNamespace(): NamespaceOption | null {
    // If a user explicitly logs in to the 'root' namespace, the path is set to 'root'.
    // The root namespace doesn't have a set path, so when verifying the selected namespace, it returns null.
    // Adding a check here, so if the namespace is 'root', it'll be set it to an empty string to match the root namespace.
    return (
      this.getSelected(this.allNamespaces, this.namespace?.path === 'root' ? '' : this.namespace?.path) ??
      null
    );
  }

  private matchesPath(option: NamespaceOption, currentPath: string): boolean {
    return option?.path === currentPath;
  }

  private getSelected(options: NamespaceOption[], currentPath: string): NamespaceOption | undefined {
    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)
     *   - 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)
     *
     *  Example:

Review comment (security):
it seems like we should perhaps address this further up in the namespace service? So that the path is consistently an empty string even if a user types in `root` to the login form

```js
  setNamespace(path) {
    // technically users can input `root` in the login form, but we consistently want root to be an empty string
    if (!path || sanitizePath(path) === 'root') {
      this.path = '';
      return;
    }
    this.path = path;
  }

Code after:

  @tracked canManageNamespaces = false; // Show/hide manage namespaces button
  @tracked canRefreshNamespaces = false; // Show/hide refresh list button
  @tracked errorLoadingNamespaces = '';
  @tracked hasNamespaces = false;
  @tracked searchInput = '';
  @tracked searchInputHelpText =
    "Enter a full path in the search bar and hit the 'Enter' ↵ key to navigate faster.";

  constructor(owner: unknown, args: Record<string, never>) {
    super(owner, args);
    this.loadOptions();
  }

  get allNamespaces(): NamespaceOption[] {
    return this.getOptions(
      this.namespace?.accessibleNamespaces,
      this.namespace?.currentNamespace,
      this.namespace?.path
    );
  }

  get selectedNamespace(): NamespaceOption | null {
    return this.getSelected(this.allNamespaces, this.namespace?.path) ?? null;
  }

  private matchesPath(option: NamespaceOption, currentPath: string): boolean {
    return option?.path === currentPath;
  }

  private getSelected(options: NamespaceOption[], currentPath: string): NamespaceOption | undefined {
    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)
     *   - 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)
     *
     *  Example:
     *   | id       | path           | label          |
     *   | ---      | ----           | -----          |
     *   | 'root'   | ''             | 'root'         |
     *   | 'parent' | 'parent'       | 'parent'       |
     *   | 'child'  | 'parent/child' | 'parent/child' |
     */