1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/renderer/components/+user-management/+role-bindings/dialog.tsx
Sebastian Malton f594bf03f3 Turn on strict mode in tsconfig.json
- Add route, clusterRoute, and payloadValidatedClusterRoute helper
  functions to improve types with backend routes

- Turn on the following new lints:
  - react/jsx-first-prop-new-line
  - react/jsx-wrap-multilines
  - react/jsx-one-expression-per-line
  - react/jsx-max-props-per-line
  - react/jsx-indent
  - react/jsx-indent-props

Signed-off-by: Sebastian Malton <sebastian@malton.name>
2022-05-04 08:45:50 -04:00

287 lines
8.7 KiB
TypeScript

/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./dialog.scss";
import { computed, observable, makeObservable, action } from "mobx";
import { observer } from "mobx-react";
import React from "react";
import { rolesStore } from "../+roles/store";
import { serviceAccountsStore } from "../+service-accounts/store";
import { NamespaceSelect } from "../../+namespaces/namespace-select";
import type { ClusterRole, Role, RoleBinding, ServiceAccount } from "../../../../common/k8s-api/endpoints";
import { roleApi } from "../../../../common/k8s-api/endpoints";
import type { DialogProps } from "../../dialog";
import { Dialog } from "../../dialog";
import { EditableList } from "../../editable-list";
import { Icon } from "../../icon";
import { showDetails } from "../../kube-detail-params";
import { SubTitle } from "../../layout/sub-title";
import { Notifications } from "../../notifications";
import { Select } from "../../select";
import { Wizard, WizardStep } from "../../wizard";
import { roleBindingStore } from "./store";
import { clusterRolesStore } from "../+cluster-roles/store";
import { Input } from "../../input";
import { ObservableHashSet, nFircate } from "../../../utils";
import type { Subject } from "../../../../common/k8s-api/endpoints/types/subject";
export interface RoleBindingDialogProps extends Partial<DialogProps> {
}
interface DialogState {
isOpen: boolean;
data?: RoleBinding;
}
@observer
export class RoleBindingDialog extends React.Component<RoleBindingDialogProps> {
static state = observable.object<DialogState>({
isOpen: false,
});
constructor(props: RoleBindingDialogProps) {
super(props);
makeObservable(this);
}
static open(roleBinding?: RoleBinding) {
RoleBindingDialog.state.isOpen = true;
RoleBindingDialog.state.data = roleBinding;
}
static close() {
RoleBindingDialog.state.isOpen = false;
RoleBindingDialog.state.data = undefined;
}
get roleBinding() {
return RoleBindingDialog.state.data;
}
@computed get isEditing() {
return !!this.roleBinding;
}
@observable.ref selectedRoleRef: Role | ClusterRole | null | undefined = null;
@observable bindingName = "";
@observable bindingNamespace: string | null = null;
selectedAccounts = new ObservableHashSet<ServiceAccount>([], sa => sa.metadata.uid);
selectedUsers = observable.set<string>([]);
selectedGroups = observable.set<string>([]);
@computed get selectedBindings(): Subject[] {
const serviceAccounts: Subject[] = Array.from(this.selectedAccounts, sa => ({
name: sa.getName(),
kind: "ServiceAccount",
namespace: sa.getNs(),
}));
const users: Subject[] = Array.from(this.selectedUsers, user => ({
name: user,
kind: "User",
}));
const groups: Subject[] = Array.from(this.selectedGroups, group => ({
name: group,
kind: "Group",
}));
return [
...serviceAccounts,
...users,
...groups,
];
}
@computed get roleRefOptions(): (Role | ClusterRole)[] {
const roles = rolesStore.items
.filter(role => role.getNs() === this.bindingNamespace);
const clusterRoles = clusterRolesStore.items;
return [
...roles,
...clusterRoles,
];
}
onOpen = action(() => {
const binding = this.roleBinding;
if (!binding) {
return this.reset();
}
const findByRoleRefName = (item: Role | ClusterRole) => item.getName() === binding.roleRef.name;
this.selectedRoleRef = (
binding.roleRef.kind === roleApi.kind
? rolesStore.items.find(findByRoleRefName)
: clusterRolesStore.items.find(findByRoleRefName)
);
this.bindingName = binding.getName();
this.bindingNamespace = binding.getNs();
const [saSubjects, uSubjects, gSubjects] = nFircate(binding.getSubjects(), "kind", ["ServiceAccount", "User", "Group"]);
const accountNames = new Set(saSubjects.map(acc => acc.name));
this.selectedAccounts.replace(
serviceAccountsStore.items
.filter(sa => accountNames.has(sa.getName())),
);
this.selectedUsers.replace(uSubjects.map(user => user.name));
this.selectedGroups.replace(gSubjects.map(group => group.name));
});
reset = action(() => {
this.selectedRoleRef = null;
this.bindingName = "";
this.bindingNamespace = "";
this.selectedAccounts.clear();
this.selectedUsers.clear();
this.selectedGroups.clear();
});
createBindings = async () => {
const { selectedRoleRef, bindingNamespace, selectedBindings, roleBinding, bindingName } = this;
if (!selectedRoleRef || !roleBinding || !bindingNamespace || !bindingName) {
return;
}
try {
const newRoleBinding = this.isEditing
? await roleBindingStore.updateSubjects(roleBinding, selectedBindings)
: await roleBindingStore.create({
name: bindingName,
namespace: bindingNamespace,
}, {
subjects: selectedBindings,
roleRef: {
name: selectedRoleRef.getName(),
kind: selectedRoleRef.kind,
},
});
showDetails(newRoleBinding.selfLink);
RoleBindingDialog.close();
} catch (err) {
Notifications.checkedError(err, `Unknown error occured while ${this.isEditing ? "editing" : "creating"} role bindings.`);
}
};
renderContents() {
return (
<>
<SubTitle title="Namespace" />
<NamespaceSelect
id="dialog-namespace-input"
themeName="light"
isDisabled={this.isEditing}
value={this.bindingNamespace}
autoFocus={!this.isEditing}
onChange={namespace => this.bindingNamespace = namespace}
/>
<SubTitle title="Role Reference" />
<Select
id="role-reference-input"
themeName="light"
placeholder="Select role or cluster role ..."
isDisabled={this.isEditing}
options={this.roleRefOptions}
value={this.selectedRoleRef}
getOptionLabel={ref => ref.getName()}
onChange={roleRef => {
this.selectedRoleRef = roleRef;
if (!this.selectedRoleRef || this.bindingName === this.selectedRoleRef.getName()) {
this.bindingName = roleRef?.getName() ?? "";
}
}}
/>
<SubTitle title="Binding Name" />
<Input
disabled={this.isEditing}
value={this.bindingName}
onChange={value => this.bindingName = value}
/>
<SubTitle title="Binding targets" />
<b>Users</b>
<EditableList
placeholder="Bind to User Account ..."
add={(newUser) => this.selectedUsers.add(newUser)}
items={Array.from(this.selectedUsers)}
remove={({ oldItem }) => this.selectedUsers.delete(oldItem)}
/>
<b>Groups</b>
<EditableList
placeholder="Bind to User Group ..."
add={(newGroup) => this.selectedGroups.add(newGroup)}
items={Array.from(this.selectedGroups)}
remove={({ oldItem }) => this.selectedGroups.delete(oldItem)}
/>
<b>Service Accounts</b>
<Select
id="service-account-input"
isMulti
themeName="light"
placeholder="Select service accounts ..."
options={serviceAccountsStore.items}
isOptionSelected={account => this.selectedAccounts.has(account)}
formatOptionLabel={account => (
<>
<Icon small material="account_box" />
{` ${account.getName()} (${account.getNs()})`}
</>
)}
getOptionLabel={account => `${account.getName()} (${account.getNs()})`}
onChange={selected => this.selectedAccounts.replace(selected)}
maxMenuHeight={200}
/>
</>
);
}
render() {
const { ...dialogProps } = this.props;
const [action, nextLabel] = this.isEditing ? ["Edit", "Update"] : ["Add", "Create"];
const disableNext = !this.selectedRoleRef || !this.selectedBindings.length || !this.bindingNamespace || !this.bindingName;
return (
<Dialog
{...dialogProps}
className="AddRoleBindingDialog"
isOpen={RoleBindingDialog.state.isOpen}
close={RoleBindingDialog.close}
onClose={this.reset}
onOpen={this.onOpen}
>
<Wizard
header={(
<h5>
{`${action} RoleBinding`}
</h5>
)}
done={RoleBindingDialog.close}
>
<WizardStep
nextLabel={nextLabel}
next={this.createBindings}
disabledNext={disableNext}
>
{this.renderContents()}
</WizardStep>
</Wizard>
</Dialog>
);
}
}