From 2fc588aed5e416702c802c9529cd7d2815b05ba2 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Wed, 17 Nov 2021 08:01:40 -0500 Subject: [PATCH] Fix dropdowns not being searchable for ClusterRoleBindingDialog and RoleBindingDialog (#4272) * Fix dropdowns not being searchable for ClusterRoleBindingDialog and RoleBindingDialog - Added some tests Signed-off-by: Sebastian Malton * Fix unit test Signed-off-by: Sebastian Malton --- .../__tests__/dialog.test.tsx} | 63 +++++++++++------- .../+cluster-role-bindings/dialog.tsx | 49 +++++++++----- .../+role-bindings/__tests__/dialog.test.tsx | 65 +++++++++++++++++++ .../+role-bindings/dialog.tsx | 33 +++++----- 4 files changed, 150 insertions(+), 60 deletions(-) rename src/renderer/components/+user-management/{select-options.tsx => +cluster-role-bindings/__tests__/dialog.test.tsx} (50%) create mode 100644 src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx diff --git a/src/renderer/components/+user-management/select-options.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx similarity index 50% rename from src/renderer/components/+user-management/select-options.tsx rename to src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx index 16608f9f07..132f338685 100644 --- a/src/renderer/components/+user-management/select-options.tsx +++ b/src/renderer/components/+user-management/+cluster-role-bindings/__tests__/dialog.test.tsx @@ -19,31 +19,44 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +import { render } from "@testing-library/react"; import React from "react"; -import type { ServiceAccount } from "../../../common/k8s-api/endpoints"; -import type { KubeObject } from "../../../common/k8s-api/kube-object"; -import { Icon } from "../icon"; -import type { SelectOption } from "../select"; -import { TooltipPosition } from "../tooltip"; +import { ClusterRoleBindingDialog } from "../dialog"; +import { clusterRolesStore } from "../../+cluster-roles/store"; +import { ClusterRole } from "../../../../../common/k8s-api/endpoints"; +import userEvent from "@testing-library/user-event"; -export type ServiceAccountOption = SelectOption & { account: ServiceAccount }; +jest.mock("../../+cluster-roles/store"); -export function getRoleRefSelectOption(item: T): SelectOption { - return { - value: item, - label: ( - <> - - {" "} - {item.getName()} - - ), - }; -} +describe("ClusterRoleBindingDialog tests", () => { + beforeEach(() => { + (clusterRolesStore as any).items = [new ClusterRole({ + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "ClusterRole", + metadata: { + name: "foobar", + resourceVersion: "1", + uid: "1", + }, + })]; + }); + + afterEach(() => { + ClusterRoleBindingDialog.close(); + jest.resetAllMocks(); + }); + + it("should render without any errors", () => { + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("clusterrole select should be searchable", async () => { + ClusterRoleBindingDialog.open(); + const res = render(); + + userEvent.keyboard("a"); + await res.findAllByText("foobar"); + }); +}); diff --git a/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx b/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx index cac74775e8..7b494af70d 100644 --- a/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx +++ b/src/renderer/components/+user-management/+cluster-role-bindings/dialog.tsx @@ -37,9 +37,9 @@ import { Select, SelectOption } from "../../select"; import { Wizard, WizardStep } from "../../wizard"; import { clusterRoleBindingsStore } from "./store"; import { clusterRolesStore } from "../+cluster-roles/store"; -import { getRoleRefSelectOption, ServiceAccountOption } from "../select-options"; import { ObservableHashSet, nFircate } from "../../../utils"; import { Input } from "../../input"; +import { TooltipPosition } from "../../tooltip"; interface Props extends Partial { } @@ -114,24 +114,21 @@ export class ClusterRoleBindingDialog extends React.Component { } @computed get clusterRoleRefoptions(): SelectOption[] { - return clusterRolesStore.items.map(getRoleRefSelectOption); + return clusterRolesStore.items.map(value => ({ + value, + label: value.getName(), + })); } - @computed get serviceAccountOptions(): ServiceAccountOption[] { - return serviceAccountsStore.items.map(account => { - const name = account.getName(); - const namespace = account.getNs(); - - return { - value: `${account.getName()}%${account.getNs()}`, - account, - label: <> {name} ({namespace}), - }; - }); + @computed get serviceAccountOptions(): SelectOption[] { + return serviceAccountsStore.items.map(account => ({ + value: account, + label: `${account.getName()} (${account.getNs()})`, + })); } - @computed get selectedServiceAccountOptions(): ServiceAccountOption[] { - return this.serviceAccountOptions.filter(({ account }) => this.selectedAccounts.has(account)); + @computed get selectedServiceAccountOptions(): SelectOption[] { + return this.serviceAccountOptions.filter(({ value }) => this.selectedAccounts.has(value)); } @action @@ -198,6 +195,21 @@ export class ClusterRoleBindingDialog extends React.Component { isDisabled={this.isEditing} options={this.clusterRoleRefoptions} value={this.selectedRoleRef} + autoFocus={!this.isEditing} + formatOptionLabel={({ value }: SelectOption) => ( + <> + + {" "} + {value.getName()} + + )} onChange={({ value }: SelectOption ) => { if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) { this.bindingName = value.getName(); @@ -241,9 +253,12 @@ export class ClusterRoleBindingDialog extends React.Component { autoConvertOptions={false} options={this.serviceAccountOptions} value={this.selectedServiceAccountOptions} - onChange={(selected: ServiceAccountOption[] | null) => { + formatOptionLabel={({ value }: SelectOption) => ( + <> {value.getName()} ({value.getNs()}) + )} + onChange={(selected: SelectOption[] | null) => { if (selected) { - this.selectedAccounts.replace(selected.map(opt => opt.account)); + this.selectedAccounts.replace(selected.map(opt => opt.value)); } else { this.selectedAccounts.clear(); } diff --git a/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx b/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx new file mode 100644 index 0000000000..5b66d7a8c7 --- /dev/null +++ b/src/renderer/components/+user-management/+role-bindings/__tests__/dialog.test.tsx @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2021 OpenLens Authors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import React from "react"; +import { clusterRolesStore } from "../../+cluster-roles/store"; +import { ClusterRole } from "../../../../../common/k8s-api/endpoints"; +import { RoleBindingDialog } from "../dialog"; + +jest.mock("../../+cluster-roles/store"); + +describe("RoleBindingDialog tests", () => { + beforeEach(() => { + (clusterRolesStore as any).items = [new ClusterRole({ + apiVersion: "rbac.authorization.k8s.io/v1", + kind: "ClusterRole", + metadata: { + name: "foobar", + resourceVersion: "1", + uid: "1", + }, + })]; + }); + + afterEach(() => { + RoleBindingDialog.close(); + jest.resetAllMocks(); + }); + + it("should render without any errors", () => { + const { container } = render(); + + expect(container).toBeInstanceOf(HTMLElement); + }); + + it("role select should be searchable", async () => { + RoleBindingDialog.open(); + const res = render(); + + userEvent.click(await res.findByText("Select role", { exact: false })); + + await res.findAllByText("foobar", { + exact: false, + }); + }); +}); diff --git a/src/renderer/components/+user-management/+role-bindings/dialog.tsx b/src/renderer/components/+user-management/+role-bindings/dialog.tsx index 8da322297c..a04a888bfe 100644 --- a/src/renderer/components/+user-management/+role-bindings/dialog.tsx +++ b/src/renderer/components/+user-management/+role-bindings/dialog.tsx @@ -40,7 +40,6 @@ import { Wizard, WizardStep } from "../../wizard"; import { roleBindingsStore } from "./store"; import { clusterRolesStore } from "../+cluster-roles/store"; import { Input } from "../../input"; -import { getRoleRefSelectOption, ServiceAccountOption } from "../select-options"; import { ObservableHashSet, nFircate } from "../../../utils"; interface Props extends Partial { @@ -112,9 +111,9 @@ export class RoleBindingDialog extends React.Component { @computed get roleRefOptions(): SelectOption[] { const roles = rolesStore.items .filter(role => role.getNs() === this.bindingNamespace) - .map(getRoleRefSelectOption); + .map(value => ({ value, label: value.getName() })); const clusterRoles = clusterRolesStore.items - .map(getRoleRefSelectOption); + .map(value => ({ value, label: value.getName() })); return [ ...roles, @@ -122,21 +121,15 @@ export class RoleBindingDialog extends React.Component { ]; } - @computed get serviceAccountOptions(): ServiceAccountOption[] { - return serviceAccountsStore.items.map(account => { - const name = account.getName(); - const namespace = account.getNs(); - - return { - value: `${account.getName()}%${account.getNs()}`, - account, - label: <> {name} ({namespace}), - }; - }); + @computed get serviceAccountOptions(): SelectOption[] { + return serviceAccountsStore.items.map(account => ({ + value: account, + label: `${account.getName()} (${account.getNs()})`, + })); } - @computed get selectedServiceAccountOptions(): ServiceAccountOption[] { - return this.serviceAccountOptions.filter(({ account }) => this.selectedAccounts.has(account)); + @computed get selectedServiceAccountOptions(): SelectOption[] { + return this.serviceAccountOptions.filter(({ value }) => this.selectedAccounts.has(value)); } @action @@ -204,6 +197,7 @@ export class RoleBindingDialog extends React.Component { themeName="light" isDisabled={this.isEditing} value={this.bindingNamespace} + autoFocus={!this.isEditing} onChange={({ value }) => this.bindingNamespace = value} /> @@ -256,9 +250,12 @@ export class RoleBindingDialog extends React.Component { autoConvertOptions={false} options={this.serviceAccountOptions} value={this.selectedServiceAccountOptions} - onChange={(selected: ServiceAccountOption[] | null) => { + formatOptionLabel={({ value }: SelectOption) => ( + <> {value.getName()} ({value.getNs()}) + )} + onChange={(selected: SelectOption[] | null) => { if (selected) { - this.selectedAccounts.replace(selected.map(opt => opt.account)); + this.selectedAccounts.replace(selected.map(opt => opt.value)); } else { this.selectedAccounts.clear(); }