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 { KubeStatus } from "./kube-object";
import type { IKubeWatchEvent } from "./kube-watch-event"; import type { IKubeWatchEvent } from "./kube-watch-event";
import { ItemStore } from "../item.store"; 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 { parseKubeApi } from "./kube-api-parse";
import type { RequestInit } from "node-fetch"; import type { RequestInit } from "node-fetch";
import type { Patch } from "rfc6902"; import type { Patch } from "rfc6902";
@ -327,12 +327,12 @@ export abstract class KubeObjectStore<
if (error) this.reset(); 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); return this.api.get(params);
} }
@action @action
async load(params: { name: string; namespace?: string }): Promise<K> { async load(params: ResourceDescriptor): Promise<K> {
const { name, namespace } = params; const { name, namespace } = params;
let item: K | null | undefined = this.getByName(name, namespace); let item: K | null | undefined = this.getByName(name, namespace);
@ -356,11 +356,11 @@ export abstract class KubeObjectStore<
return this.load({ name, namespace }); 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); 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); const newItem = await this.createItem(params, data);
assert(newItem, "Failed to create item from kube"); 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 "./namespace-select.scss";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import type { IComputedValue } from "mobx";
import { computed } from "mobx"; import { computed } from "mobx";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import type { SelectProps } from "../select"; import type { SelectProps } from "../select";
import { Select } from "../select"; import { Select } from "../select";
import { cssNames } from "../../utils"; import { cssNames } from "../../utils";
import { Icon } from "../icon"; import { Icon } from "../icon";
import type { NamespaceStore } from "./store";
import { withInjectables } from "@ogre-tools/injectable-react"; 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; export type NamespaceSelectSort = (left: string, right: string) => number;
@ -25,12 +25,12 @@ export interface NamespaceSelectProps<IsMulti extends boolean> extends Omit<Sele
} }
interface Dependencies { interface Dependencies {
namespaceStore: NamespaceStore; namespaces: IComputedValue<string[]>;
} }
function getOptions(namespaceStore: NamespaceStore, sort: NamespaceSelectSort | undefined) { function getOptions(namespaces: IComputedValue<string[]>, sort: NamespaceSelectSort | undefined) {
return computed(() => { return computed(() => {
const baseOptions = namespaceStore.items.map(ns => ns.getName()); const baseOptions = namespaces.get();
if (sort) { if (sort) {
baseOptions.sort(sort); baseOptions.sort(sort);
@ -44,16 +44,16 @@ function getOptions(namespaceStore: NamespaceStore, sort: NamespaceSelectSort |
} }
const NonInjectedNamespaceSelect = observer(({ const NonInjectedNamespaceSelect = observer(({
namespaceStore, namespaces,
showIcons, showIcons,
formatOptionLabel, formatOptionLabel,
sort, sort,
className, className,
...selectProps ...selectProps
}: Dependencies & NamespaceSelectProps<boolean>) => { }: 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 ( return (
<Select <Select
@ -77,7 +77,7 @@ const NonInjectedNamespaceSelect = observer(({
const InjectedNamespaceSelect = withInjectables<Dependencies, NamespaceSelectProps<boolean>>(NonInjectedNamespaceSelect, { const InjectedNamespaceSelect = withInjectables<Dependencies, NamespaceSelectProps<boolean>>(NonInjectedNamespaceSelect, {
getProps: (di, props) => ({ getProps: (di, props) => ({
...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"; import navigateToNamespacesInjectable from "../../../common/front-end-routing/routes/cluster/namespaces/navigate-to-namespaces.injectable";
const namespacesSidebarItemsInjectable = getInjectable({ const namespacesSidebarItemsInjectable = getInjectable({
id: "namespaces", id: "namespaces-sidebar-items",
instantiate: (di) => { instantiate: (di) => {
const route = di.inject(namespacesRouteInjectable); 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 React from "react";
import { KubeObjectListLayout } from "../../kube-object-list-layout"; import { KubeObjectListLayout } from "../../kube-object-list-layout";
import { KubeObjectStatusIcon } from "../../kube-object-status-icon"; import { KubeObjectStatusIcon } from "../../kube-object-status-icon";
import { AddRoleDialog } from "./add-dialog";
import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout"; import { SiblingsInTabLayout } from "../../layout/siblings-in-tab-layout";
import { KubeObjectAge } from "../../kube-object/age"; import { KubeObjectAge } from "../../kube-object/age";
import type { RoleStore } from "./store"; 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 { withInjectables } from "@ogre-tools/injectable-react";
import filterByNamespaceInjectable from "../../+namespaces/namespace-select-filter-model/filter-by-namespace.injectable"; import filterByNamespaceInjectable from "../../+namespaces/namespace-select-filter-model/filter-by-namespace.injectable";
import roleStoreInjectable from "./store.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 { enum columnId {
name = "name", name = "name",
@ -28,65 +30,62 @@ enum columnId {
interface Dependencies { interface Dependencies {
roleStore: RoleStore; roleStore: RoleStore;
filterByNamespace: FilterByNamespace; filterByNamespace: FilterByNamespace;
openAddRoleDialog: OpenAddRoleDialog;
} }
@observer const NonInjectedRoles = observer(({
class NonInjectedRoles extends React.Component<Dependencies> { roleStore,
render() { openAddRoleDialog,
const { filterByNamespace,
filterByNamespace, }: Dependencies) => (
roleStore, <SiblingsInTabLayout>
} = this.props; <KubeObjectListLayout
isConfigurable
return ( tableId="access_roles"
<SiblingsInTabLayout> className="Roles"
<KubeObjectListLayout store={roleStore}
isConfigurable sortingCallbacks={{
tableId="access_roles" [columnId.name]: role => role.getName(),
className="Roles" [columnId.namespace]: role => role.getNs(),
store={roleStore} [columnId.age]: role => -role.getCreationTimestamp(),
sortingCallbacks={{ }}
[columnId.name]: role => role.getName(), searchFilters={[
[columnId.namespace]: role => role.getNs(), role => role.getSearchFields(),
[columnId.age]: role => -role.getCreationTimestamp(), ]}
}} renderHeaderTitle="Roles"
searchFilters={[ renderTableHeader={[
role => role.getSearchFields(), { title: "Name", className: "name", sortBy: columnId.name, id: columnId.name },
]} { className: "warning", showWithColumn: columnId.name },
renderHeaderTitle="Roles" { title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace },
renderTableHeader={[ { title: "Age", className: "age", sortBy: columnId.age, id: columnId.age },
{ title: "Name", className: "name", sortBy: columnId.name, id: columnId.name }, ]}
{ className: "warning", showWithColumn: columnId.name }, renderTableContents={role => [
{ title: "Namespace", className: "namespace", sortBy: columnId.namespace, id: columnId.namespace }, role.getName(),
{ title: "Age", className: "age", sortBy: columnId.age, id: columnId.age }, <KubeObjectStatusIcon key="icon" object={role} />,
]} <a
renderTableContents={role => [ key="namespace"
role.getName(), className="filterNamespace"
<KubeObjectStatusIcon key="icon" object={role} />, onClick={prevDefault(() => filterByNamespace(role.getNs()))}
<a >
key="namespace" {role.getNs()}
className="filterNamespace" </a>,
onClick={prevDefault(() => filterByNamespace(role.getNs()))} <KubeObjectAge key="age" object={role} />,
> ]}
{role.getNs()} addRemoveButtons={{
</a>, onAdd: openAddRoleDialog,
<KubeObjectAge key="age" object={role} />, addTooltip: "Create new Role",
]} "data-testid": "roles-view",
addRemoveButtons={{ }}
onAdd: () => AddRoleDialog.open(), />
addTooltip: "Create new Role", <AddRoleDialog />
}} </SiblingsInTabLayout>
/> ));
<AddRoleDialog/>
</SiblingsInTabLayout>
);
}
}
export const Roles = withInjectables<Dependencies>(NonInjectedRoles, { export const Roles = withInjectables<Dependencies>(NonInjectedRoles, {
getProps: (di, props) => ({ getProps: (di, props) => ({
...props, ...props,
filterByNamespace: di.inject(filterByNamespaceInjectable), openAddRoleDialog: di.inject(openAddRoleDialogInjectable),
roleStore: di.inject(roleStoreInjectable), roleStore: di.inject(roleStoreInjectable),
filterByNamespace: di.inject(filterByNamespaceInjectable),
}), }),
}); });

View File

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

View File

@ -5,7 +5,7 @@
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import { kubeObjectDetailItemInjectionToken } from "../kube-object-detail-item-injection-token"; import { kubeObjectDetailItemInjectionToken } from "../kube-object-detail-item-injection-token";
import { computed } from "mobx"; 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 { kubeObjectMatchesToKindAndApiVersion } from "../kube-object-matches-to-kind-and-api-version";
import currentKubeObjectInDetailsInjectable from "../../current-kube-object-in-details.injectable"; 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()) { if (this.form.noValidate || this.form.checkValidity()) {
this.next(); this.next();
} }
}, 100); }, 100, {
leading: true,
});
renderLoading() { renderLoading() {
return ( return (