1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Fix AddRoleDialog's create button being enabled before entering data

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-07-26 09:14:29 -04:00
parent 9ba92cb072
commit 019414182f
17 changed files with 6186 additions and 219 deletions

View File

@ -12,7 +12,7 @@ import type { KubeJsonApiDataFor, KubeObject } from "./kube-object";
import { KubeStatus } from "./kube-object";
import type { IKubeWatchEvent } from "./kube-watch-event";
import { ItemStore } from "../item.store";
import type { KubeApiQueryParams, KubeApi, KubeApiWatchCallback } from "./kube-api";
import type { KubeApiQueryParams, KubeApi, KubeApiWatchCallback, ResourceDescriptor } from "./kube-api";
import { parseKubeApi } from "./kube-api-parse";
import type { RequestInit } from "node-fetch";
import type { Patch } from "rfc6902";
@ -327,12 +327,12 @@ export abstract class KubeObjectStore<
if (error) this.reset();
}
protected async loadItem(params: { name: string; namespace?: string }): Promise<K | null> {
protected async loadItem(params: ResourceDescriptor): Promise<K | null> {
return this.api.get(params);
}
@action
async load(params: { name: string; namespace?: string }): Promise<K> {
async load(params: ResourceDescriptor): Promise<K> {
const { name, namespace } = params;
let item: K | null | undefined = this.getByName(name, namespace);
@ -356,11 +356,11 @@ export abstract class KubeObjectStore<
return this.load({ name, namespace });
}
protected async createItem(params: { name: string; namespace?: string }, data?: PartialDeep<K>): Promise<K | null> {
protected async createItem(params: ResourceDescriptor, data?: PartialDeep<K>): Promise<K | null> {
return this.api.create(params, data);
}
async create(params: { name: string; namespace?: string }, data?: PartialDeep<K>): Promise<K> {
async create(params: ResourceDescriptor, data?: PartialDeep<K>): Promise<K> {
const newItem = await this.createItem(params, data);
assert(newItem, "Failed to create item from kube");

View File

@ -0,0 +1,187 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { RenderResult } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { computed } from "mobx";
import type { PartialDeep } from "type-fest";
import { Role } from "../../../../common/k8s-api/endpoints";
import type { ResourceDescriptor } from "../../../../common/k8s-api/kube-api";
import namespacesInjectable from "../../../../renderer/components/+namespaces/namespaces.injectable";
import navigateToRolesViewInjectable from "../../../../renderer/components/+user-management/+roles/navigate-to.injectable";
import roleStoreInjectable from "../../../../renderer/components/+user-management/+roles/store.injectable";
import type { ShowDetails } from "../../../../renderer/components/kube-detail-params/show-details.injectable";
import showDetailsInjectable from "../../../../renderer/components/kube-detail-params/show-details.injectable";
import type { ShowNotification } from "../../../../renderer/components/notifications";
import showErrorNotificationInjectable from "../../../../renderer/components/notifications/show-error-notification.injectable";
import type { ApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../../renderer/components/test-utils/get-application-builder";
describe("Roles view/add new role dialog", () => {
let applicationBuilder: ApplicationBuilder;
let renderResult: RenderResult;
let createRoleMock: AsyncFnMock<(params: ResourceDescriptor, data?: PartialDeep<Role>) => Promise<Role>>;
let showDetailsMock: jest.MockedFunction<ShowDetails>;
let showErrorNotificationMock: jest.MockedFunction<ShowNotification>;
beforeEach(async () => {
applicationBuilder = getApplicationBuilder();
applicationBuilder.setEnvironmentToClusterFrame();
applicationBuilder.allowKubeResource("roles");
applicationBuilder.beforeWindowStart((windowDi) => {
windowDi.override(namespacesInjectable, () => computed(() => (
["default", "my-namespace", "my-namespace-2"]
)));
createRoleMock = asyncFn();
showDetailsMock = jest.fn();
showErrorNotificationMock = jest.fn();
windowDi.inject(roleStoreInjectable).create = createRoleMock;
windowDi.override(showDetailsInjectable, () => showDetailsMock);
windowDi.override(showErrorNotificationInjectable, () => showErrorNotificationMock);
windowDi.inject(navigateToRolesViewInjectable)();
});
renderResult = await applicationBuilder.render();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("does not show the add role dialog yet", () => {
expect(renderResult.queryByTestId("add-role-dialog")).not.toBeInTheDocument();
});
describe("when add new role button is clicked", () => {
beforeEach(() => {
renderResult.getByTestId("roles-view-add-button").click();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("shows the add role dialog", () => {
expect(renderResult.queryByTestId("add-role-dialog")).toBeInTheDocument();
});
it("create role button is disabled", () => {
expect(renderResult.getByTestId("add-role-dialog-create-step")).toBeDisabled();
});
describe("with name inputed", () => {
beforeEach(() => {
userEvent.type(
renderResult.getByTestId("add-role-dialog-name-input"),
"my-role-name",
);
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("create role button is disabled", () => {
expect(renderResult.getByTestId("add-role-dialog-create-step")).toBeDisabled();
});
describe("with namespace selected", () => {
beforeEach(() => {
applicationBuilder.select
.openMenu("add-dialog-namespace-select-input")
.selectOption("default");
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("create role button is enabled", () => {
expect(renderResult.getByTestId("add-role-dialog-create-step")).toBeEnabled();
});
describe("when create button is clicked", () => {
beforeEach(() => {
renderResult.getByTestId("add-role-dialog-create-step").click();
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("calls roleStore.create", () => {
expect(createRoleMock).toBeCalledWith({
name: "my-role-name",
namespace: "default",
});
});
it("still shows the dialog", () => {
expect(renderResult.queryByTestId("add-role-dialog")).toBeInTheDocument();
});
it("shows the create button as loading", () => {
expect(renderResult.getByTestId("add-role-dialog-create-step").getAttribute("data-waiting")).toBe("true");
});
describe("when roleStore.create resolves", () => {
beforeEach(async () => {
await createRoleMock.resolve(new Role({
apiVersion: "rbac.authorization.k8s.io/v1",
kind: "Role",
metadata: {
name: "my-role-name",
namespace: "default",
resourceVersion: "1",
selfLink: "/apis/rbac.authorization.k8s.io/v1/role/default/my-role-name",
uid: "123",
},
}));
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("no longer shows the dialog", () => {
expect(renderResult.queryByTestId("add-role-dialog")).not.toBeInTheDocument();
});
it("shows kube details", () => {
expect(showDetailsMock).toBeCalledWith("/apis/rbac.authorization.k8s.io/v1/role/default/my-role-name");
});
});
describe("when roleStore.create rejects", () => {
beforeEach(async () => {
await createRoleMock.reject("some-error");
});
it("renders", () => {
expect(renderResult.baseElement).toMatchSnapshot();
});
it("still shows the dialog", () => {
expect(renderResult.queryByTestId("add-role-dialog")).toBeInTheDocument();
});
it("no longer shows the create button as loading", () => {
expect(renderResult.getByTestId("add-role-dialog-create-step").getAttribute("data-waiting")).toBe("false");
});
it("shows an error notification", () => {
expect(showErrorNotificationMock).toBeCalledWith("some-error", undefined);
});
});
});
});
});
});
});

View File

@ -6,15 +6,15 @@
import "./namespace-select.scss";
import React, { useEffect, useState } from "react";
import type { IComputedValue } from "mobx";
import { computed } from "mobx";
import { observer } from "mobx-react";
import type { SelectProps } from "../select";
import { Select } from "../select";
import { cssNames } from "../../utils";
import { Icon } from "../icon";
import type { NamespaceStore } from "./store";
import { withInjectables } from "@ogre-tools/injectable-react";
import namespaceStoreInjectable from "./store.injectable";
import namespacesInjectable from "./namespaces.injectable";
export type NamespaceSelectSort = (left: string, right: string) => number;
@ -25,12 +25,12 @@ export interface NamespaceSelectProps<IsMulti extends boolean> extends Omit<Sele
}
interface Dependencies {
namespaceStore: NamespaceStore;
namespaces: IComputedValue<string[]>;
}
function getOptions(namespaceStore: NamespaceStore, sort: NamespaceSelectSort | undefined) {
function getOptions(namespaces: IComputedValue<string[]>, sort: NamespaceSelectSort | undefined) {
return computed(() => {
const baseOptions = namespaceStore.items.map(ns => ns.getName());
const baseOptions = namespaces.get();
if (sort) {
baseOptions.sort(sort);
@ -44,16 +44,16 @@ function getOptions(namespaceStore: NamespaceStore, sort: NamespaceSelectSort |
}
const NonInjectedNamespaceSelect = observer(({
namespaceStore,
namespaces,
showIcons,
formatOptionLabel,
sort,
className,
...selectProps
}: Dependencies & NamespaceSelectProps<boolean>) => {
const [baseOptions, setBaseOptions] = useState(getOptions(namespaceStore, sort));
const [baseOptions, setBaseOptions] = useState(getOptions(namespaces, sort));
useEffect(() => setBaseOptions(getOptions(namespaceStore, sort)), [sort]);
useEffect(() => setBaseOptions(getOptions(namespaces, sort)), [sort]);
return (
<Select
@ -77,7 +77,7 @@ const NonInjectedNamespaceSelect = observer(({
const InjectedNamespaceSelect = withInjectables<Dependencies, NamespaceSelectProps<boolean>>(NonInjectedNamespaceSelect, {
getProps: (di, props) => ({
...props,
namespaceStore: di.inject(namespaceStoreInjectable),
namespaces: di.inject(namespacesInjectable),
}),
});

View File

@ -16,7 +16,7 @@ import routeIsActiveInjectable from "../../routes/route-is-active.injectable";
import navigateToNamespacesInjectable from "../../../common/front-end-routing/routes/cluster/namespaces/navigate-to-namespaces.injectable";
const namespacesSidebarItemsInjectable = getInjectable({
id: "namespaces",
id: "namespaces-sidebar-items",
instantiate: (di) => {
const route = di.inject(namespacesRouteInjectable);

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import namespaceStoreInjectable from "./store.injectable";
const namespacesInjectable = getInjectable({
id: "namespaces",
instantiate: (di) => {
const store = di.inject(namespaceStoreInjectable);
return computed(() => store.items.map(ns => ns.getName()));
},
});
export default namespacesInjectable;

View File

@ -1,100 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./add-dialog.scss";
import React from "react";
import { observable, makeObservable } from "mobx";
import { observer } from "mobx-react";
import { NamespaceSelect } from "../../+namespaces/namespace-select";
import type { DialogProps } from "../../dialog";
import { Dialog } from "../../dialog";
import { Input } from "../../input";
import { showDetails } from "../../kube-detail-params";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import { Wizard, WizardStep } from "../../wizard";
import { roleStore } from "./legacy-store";
export interface AddRoleDialogProps extends Partial<DialogProps> {
}
@observer
export class AddRoleDialog extends React.Component<AddRoleDialogProps> {
static isOpen = observable.box(false);
@observable roleName = "";
@observable namespace = "";
constructor(props: AddRoleDialogProps) {
super(props);
makeObservable(this);
}
static open() {
AddRoleDialog.isOpen.set(true);
}
static close() {
AddRoleDialog.isOpen.set(false);
}
reset = () => {
this.roleName = "";
this.namespace = "";
};
createRole = async () => {
try {
const role = await roleStore.create({ name: this.roleName, namespace: this.namespace });
showDetails(role.selfLink);
this.reset();
AddRoleDialog.close();
} catch (err) {
Notifications.checkedError(err, "Unknown error occured while creating role");
}
};
render() {
const { ...dialogProps } = this.props;
const header = <h5>Create Role</h5>;
return (
<Dialog
{...dialogProps}
className="AddRoleDialog"
isOpen={AddRoleDialog.isOpen.get()}
close={AddRoleDialog.close}
>
<Wizard header={header} done={AddRoleDialog.close}>
<WizardStep
contentClass="flex gaps column"
nextLabel="Create"
next={this.createRole}
>
<SubTitle title="Role Name" />
<Input
required
autoFocus
placeholder="Name"
iconLeft="supervisor_account"
value={this.roleName}
onChange={v => this.roleName = v}
/>
<SubTitle title="Namespace" />
<NamespaceSelect
id="add-dialog-namespace-select-input"
themeName="light"
value={this.namespace}
onChange={option => this.namespace = option?.value ?? "default"}
/>
</WizardStep>
</Wizard>
</Dialog>
);
}
}

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { AddRoleDialogState } from "./state.injectable";
import addRoleDialogStateInjectable from "./state.injectable";
export type OpenAddRoleDialog = () => void;
const openAddRoleDialogInjectable = getInjectable({
id: "open-add-role-dialog",
instantiate: (di): OpenAddRoleDialog => {
const state = di.inject(addRoleDialogStateInjectable);
return () => state.set(getBlankAddRoleDialogState());
},
});
const getBlankAddRoleDialogState = (): AddRoleDialogState => ({
name: "",
namespace: "",
});
export default openAddRoleDialogInjectable;

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
export interface AddRoleDialogState {
name: string;
namespace: string;
}
const addRoleDialogStateInjectable = getInjectable({
id: "add-role-dialog-state",
instantiate: () => observable.box<AddRoleDialogState>(),
});
export default addRoleDialogStateInjectable;

View File

@ -0,0 +1,111 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./view.scss";
import React from "react";
import type { IObservableValue } from "mobx";
import { observer } from "mobx-react";
import { NamespaceSelect } from "../../../+namespaces/namespace-select";
import type { DialogProps } from "../../../dialog";
import { Dialog } from "../../../dialog";
import { Input } from "../../../input";
import { SubTitle } from "../../../layout/sub-title";
import { Wizard, WizardStep } from "../../../wizard";
import type { AddRoleDialogState } from "./state.injectable";
import type { RoleStore } from "../store";
import type { ShowDetails } from "../../../kube-detail-params/show-details.injectable";
import type { ShowCheckedErrorNotification } from "../../../notifications/show-checked-error.injectable";
import { withInjectables } from "@ogre-tools/injectable-react";
import roleStoreInjectable from "../store.injectable";
import showCheckedErrorNotificationInjectable from "../../../notifications/show-checked-error.injectable";
import showDetailsInjectable from "../../../kube-detail-params/show-details.injectable";
import addRoleDialogStateInjectable from "./state.injectable";
export interface AddRoleDialogProps extends Partial<DialogProps> {
}
interface Dependencies {
state: IObservableValue<AddRoleDialogState | undefined>;
roleStore: RoleStore;
showDetails: ShowDetails;
showCheckedErrorNotification: ShowCheckedErrorNotification;
}
const NonInjectedAddRoleDialog = observer(({
state,
roleStore,
showDetails,
showCheckedErrorNotification,
...dialogProps
}: Dependencies & AddRoleDialogProps) => {
const close = () => state.set(undefined);
const createRole = async (roleDescriptor: AddRoleDialogState) => {
try {
const role = await roleStore.create(roleDescriptor);
close();
showDetails(role.selfLink);
} catch (err) {
showCheckedErrorNotification(err, "Unknown error occured while creating role");
}
};
const currentState = state.get();
return (
<Dialog
{...dialogProps}
className="AddRoleDialog"
isOpen={Boolean(currentState)}
close={close}
data-testid={currentState && "add-role-dialog"}
>
{currentState && (
<Wizard
header={<h5>Create Role</h5>}
done={close}
>
<WizardStep
contentClass="flex gaps column"
nextLabel="Create"
disabledNext={!currentState.namespace || !currentState.name}
next={() => createRole(currentState)}
testIdForNext="add-role-dialog-create-step"
>
<SubTitle title="Role Name" />
<Input
required
autoFocus
placeholder="Name"
iconLeft="supervisor_account"
value={currentState.name}
onChange={v => currentState.name = v}
data-testid="add-role-dialog-name-input"
/>
<SubTitle title="Namespace" />
<NamespaceSelect
id="add-dialog-namespace-select-input"
themeName="light"
value={currentState.namespace}
onChange={option => currentState.namespace = option?.value ?? ""}
/>
</WizardStep>
</Wizard>
)}
</Dialog>
);
});
export const AddRoleDialog = withInjectables<Dependencies, AddRoleDialogProps>(NonInjectedAddRoleDialog, {
getProps: (di, props) => ({
...props,
roleStore: di.inject(roleStoreInjectable),
showCheckedErrorNotification: di.inject(showCheckedErrorNotificationInjectable),
showDetails: di.inject(showDetailsInjectable),
state: di.inject(addRoleDialogStateInjectable),
}),
});

View File

@ -1,7 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export * from "./view";
export * from "./details";
export * from "./add-dialog";

View File

@ -0,0 +1,19 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { navigateToRouteInjectionToken } from "../../../../common/front-end-routing/navigate-to-route-injection-token";
import rolesRouteInjectable from "../../../../common/front-end-routing/routes/cluster/user-management/roles/roles-route.injectable";
const navigateToRolesViewInjectable = getInjectable({
id: "navigate-to-roles-view",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(rolesRouteInjectable);
return () => navigateToRoute(route);
},
});
export default navigateToRolesViewInjectable;

View File

@ -9,7 +9,6 @@ import { observer } from "mobx-react";
import React from "react";
import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { AddRoleDialog } from "./add-dialog";
import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../../kube-object/age";
import type { RoleStore } from "./store";
@ -18,6 +17,9 @@ import type { FilterByNamespace } from "../../+namespaces/namespace-select-filte
import { withInjectables } from "@ogre-tools/injectable-react";
import filterByNamespaceInjectable from "../../+namespaces/namespace-select-filter-model/filter-by-namespace.injectable";
import roleStoreInjectable from "./store.injectable";
import type { OpenAddRoleDialog } from "./add-dialog/open.injectable";
import openAddRoleDialogInjectable from "./add-dialog/open.injectable";
import { AddRoleDialog } from "./add-dialog/view";
enum columnId {
name = "name",
@ -28,65 +30,62 @@ enum columnId {
interface Dependencies {
roleStore: RoleStore;
filterByNamespace: FilterByNamespace;
openAddRoleDialog: OpenAddRoleDialog;
}
@observer
class NonInjectedRoles extends React.Component<Dependencies> {
render() {
const {
filterByNamespace,
roleStore,
} = this.props;
return (
<SiblingsInTabLayout>
<KubeObjectListLayout
isConfigurable
tableId="access_roles"
className="Roles"
store={roleStore}
sortingCallbacks={{
[columnId.name]: role => role.getName(),
[columnId.namespace]: role => role.getNs(),
[columnId.age]: role => -role.getCreationTimestamp(),
}}
searchFilters={[
role => role.getSearchFields(),
]}
renderHeaderTitle="Roles"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ className: "warning", showWithColumn: columnId.name },
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={role => [
role.getName(),
<KubeObjectStatusIcon key="icon" object={role} />,
<a
key="namespace"
className="filterNamespace"
onClick={prevDefault(() => filterByNamespace(role.getNs()))}
>
{role.getNs()}
</a>,
<KubeObjectAge key="age" object={role} />,
]}
addRemoveButtons={{
onAdd: () => AddRoleDialog.open(),
addTooltip: "Create new Role",
}}
/>
<AddRoleDialog/>
</SiblingsInTabLayout>
);
}
}
const NonInjectedRoles = observer(({
roleStore,
openAddRoleDialog,
filterByNamespace,
}: Dependencies) => (
<SiblingsInTabLayout>
<KubeObjectListLayout
isConfigurable
tableId="access_roles"
className="Roles"
store={roleStore}
sortingCallbacks={{
[columnId.name]: role => role.getName(),
[columnId.namespace]: role => role.getNs(),
[columnId.age]: role => -role.getCreationTimestamp(),
}}
searchFilters={[
role => role.getSearchFields(),
]}
renderHeaderTitle="Roles"
renderTableHeader={[
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
{ className: "warning", showWithColumn: columnId.name },
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
]}
renderTableContents={role => [
role.getName(),
<KubeObjectStatusIcon key="icon" object={role} />,
<a
key="namespace"
className="filterNamespace"
onClick={prevDefault(() => filterByNamespace(role.getNs()))}
>
{role.getNs()}
</a>,
<KubeObjectAge key="age" object={role} />,
]}
addRemoveButtons={{
onAdd: openAddRoleDialog,
addTooltip: "Create new Role",
"data-testid": "roles-view",
}}
/>
<AddRoleDialog />
</SiblingsInTabLayout>
));
export const Roles = withInjectables<Dependencies>(NonInjectedRoles, {
getProps: (di, props) => ({
...props,
filterByNamespace: di.inject(filterByNamespaceInjectable),
openAddRoleDialog: di.inject(openAddRoleDialogInjectable),
roleStore: di.inject(roleStoreInjectable),
filterByNamespace: di.inject(filterByNamespaceInjectable),
}),
});

View File

@ -10,50 +10,49 @@ import { cssNames } from "../../utils";
import { Button } from "../button";
import { Icon } from "../icon";
export interface AddRemoveButtonsProps extends React.HTMLAttributes<any> {
export interface AddRemoveButtonsProps {
onAdd?: () => void;
onRemove?: () => void;
addTooltip?: React.ReactNode;
removeTooltip?: React.ReactNode;
className?: string;
"data-testid"?: string;
}
export class AddRemoveButtons extends React.PureComponent<AddRemoveButtonsProps> {
renderButtons() {
const { onRemove, onAdd, addTooltip, removeTooltip } = this.props;
return [
{
onClick: onRemove,
className: "remove-button",
icon: "remove",
tooltip: removeTooltip,
},
{
onClick: onAdd,
className: "add-button",
icon: "add",
tooltip: addTooltip,
},
]
.filter(button => button.onClick)
.map(({ icon, ...props }) => (
<Button
key={icon}
big
round
primary
{...props}
>
<Icon material={icon} />
</Button>
));
}
render() {
return (
<div className={cssNames("AddRemoveButtons flex gaps", this.props.className)}>
{this.renderButtons()}
</div>
);
}
}
export const AddRemoveButtons = ({
onRemove,
onAdd,
addTooltip,
removeTooltip,
className,
"data-testid": dataTestid,
}: AddRemoveButtonsProps) => (
<div className={cssNames("AddRemoveButtons flex gaps", className)}>
{onRemove && (
<Button
big
round
primary
className="remove-button"
tooltip={removeTooltip}
onClick={onRemove}
data-testid={dataTestid && `${dataTestid}-remove-button`}
>
<Icon material="remove" />
</Button>
)}
{onAdd && (
<Button
big
round
primary
className="add-button"
tooltip={addTooltip}
onClick={onAdd}
data-testid={dataTestid && `${dataTestid}-add-button`}
>
<Icon material="add" />
</Button>
)}
</div>
);

View File

@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable";
import { kubeObjectDetailItemInjectionToken } from "../kube-object-detail-item-injection-token";
import { computed } from "mobx";
import { RoleDetails } from "../../../+user-management/+roles";
import { RoleDetails } from "../../../+user-management/+roles/details";
import { kubeObjectMatchesToKindAndApiVersion } from "../kube-object-matches-to-kind-and-api-version";
import currentKubeObjectInDetailsInjectable from "../../current-kube-object-in-details.injectable";

View File

@ -190,7 +190,9 @@ export class WizardStep<D> extends React.Component<WizardStepProps<D>, WizardSte
if (this.form.noValidate || this.form.checkValidity()) {
this.next();
}
}, 100);
}, 100, {
leading: true,
});
renderLoading() {
return (