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-roles-bindings/add-role-binding-dialog.tsx
Roman 93e7febc94 master-merge fixes
Signed-off-by: Roman <ixrock@gmail.com>
2021-05-19 20:05:56 +03:00

319 lines
9.5 KiB
TypeScript

/**
* 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 "./add-role-binding-dialog.scss";
import React from "react";
import { computed, observable, makeObservable } from "mobx";
import { observer } from "mobx-react";
import { Dialog, DialogProps } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { Select, SelectOption } from "../select";
import { SubTitle } from "../layout/sub-title";
import type { IRoleBindingSubject, Role, RoleBinding, ServiceAccount } from "../../api/endpoints";
import { Icon } from "../icon";
import { Input } from "../input";
import { NamespaceSelect } from "../+namespaces/namespace-select";
import { Checkbox } from "../checkbox";
import { KubeObject } from "../../api/kube-object";
import { Notifications } from "../notifications";
import { rolesStore } from "../+user-management-roles/roles.store";
import { namespaceStore } from "../+namespaces/namespace.store";
import { serviceAccountsStore } from "../+user-management-service-accounts/service-accounts.store";
import { roleBindingsStore } from "./role-bindings.store";
import { showDetails } from "../kube-object";
import type { KubeObjectStore } from "../../kube-object.store";
interface BindingSelectOption extends SelectOption {
value: string; // binding name
item?: ServiceAccount | any;
subject?: IRoleBindingSubject; // used for new user/group when users-management-api not available
}
interface Props extends Partial<DialogProps> {
}
const dialogState = observable.object({
isOpen: false,
data: null as RoleBinding,
});
@observer
export class AddRoleBindingDialog extends React.Component<Props> {
constructor(props: Props) {
super(props);
makeObservable(this);
}
static open(roleBinding?: RoleBinding) {
dialogState.isOpen = true;
dialogState.data = roleBinding;
}
static close() {
dialogState.isOpen = false;
}
get roleBinding(): RoleBinding {
return dialogState.data;
}
@observable isLoading = false;
@observable selectedRoleId = "";
@observable useRoleForBindingName = true;
@observable bindingName = ""; // new role-binding name
@observable bindContext = ""; // empty value means "cluster-wide", otherwise bind to namespace
@observable selectedAccounts = observable.array<ServiceAccount>([], { deep: false });
@computed get isEditing() {
return !!this.roleBinding;
}
@computed get selectedRole() {
return rolesStore.items.find(role => role.getId() === this.selectedRoleId);
}
@computed get selectedBindings() {
return [
...this.selectedAccounts,
];
}
close = () => {
AddRoleBindingDialog.close();
};
async loadData() {
const stores: KubeObjectStore[] = [
namespaceStore,
rolesStore,
serviceAccountsStore,
];
this.isLoading = true;
await Promise.all(stores.map(store => store.reloadAll()));
this.isLoading = false;
}
onOpen = async () => {
await this.loadData();
if (this.roleBinding) {
const { name, kind } = this.roleBinding.roleRef;
const role = rolesStore.items.find(role => role.kind === kind && role.getName() === name);
if (role) {
this.selectedRoleId = role.getId();
this.bindContext = role.getNs() || "";
}
}
};
reset = () => {
this.selectedRoleId = "";
this.bindContext = "";
this.selectedAccounts.clear();
};
onBindContextChange = (namespace: string) => {
this.bindContext = namespace;
const roleContext = this.selectedRole && this.selectedRole.getNs() || "";
if (this.bindContext && this.bindContext !== roleContext) {
this.selectedRoleId = ""; // reset previously selected role for specific context
}
};
createBindings = async () => {
const { selectedRole, bindContext: namespace, selectedBindings, bindingName, useRoleForBindingName } = this;
const subjects = selectedBindings.map((item: KubeObject | IRoleBindingSubject) => {
if (item instanceof KubeObject) {
return {
name: item.getName(),
kind: item.kind,
namespace: item.getNs(),
};
}
return item;
});
try {
let roleBinding: RoleBinding;
if (this.isEditing) {
roleBinding = await roleBindingsStore.updateSubjects({
roleBinding: this.roleBinding,
addSubjects: subjects,
});
} else {
const name = useRoleForBindingName ? selectedRole.getName() : bindingName;
roleBinding = await roleBindingsStore.create({ name, namespace }, {
subjects,
roleRef: {
name: selectedRole.getName(),
kind: selectedRole.kind,
}
});
}
showDetails(roleBinding.selfLink);
this.close();
} catch (err) {
Notifications.error(err);
}
};
@computed get roleOptions(): BindingSelectOption[] {
let roles = rolesStore.items as Role[];
if (this.bindContext) {
// show only cluster-roles or roles for selected context namespace
roles = roles.filter(role => !role.getNs() || role.getNs() === this.bindContext);
}
return roles.map(role => {
const name = role.getName();
const namespace = role.getNs();
return {
value: role.getId(),
label: name + (namespace ? ` (${namespace})` : "")
};
});
}
@computed get serviceAccountOptions(): BindingSelectOption[] {
return serviceAccountsStore.items.map(account => {
const name = account.getName();
const namespace = account.getNs();
return {
item: account,
value: name,
label: <><Icon small material="account_box"/> {name} ({namespace})</>
};
});
}
renderContents() {
const unwrapBindings = (options: BindingSelectOption[]) => options.map(option => option.item || option.subject);
return (
<>
<SubTitle title="Context"/>
<NamespaceSelect
showClusterOption
themeName="light"
isDisabled={this.isEditing}
value={this.bindContext}
onChange={({ value }) => this.onBindContextChange(value)}
/>
<SubTitle title="Role"/>
<Select
key={this.selectedRoleId}
themeName="light"
placeholder="Select role.."
isDisabled={this.isEditing}
options={this.roleOptions}
value={this.selectedRoleId}
onChange={({ value }) => this.selectedRoleId = value}
/>
{
!this.isEditing && (
<>
<Checkbox
theme="light"
label="Use same name for RoleBinding"
value={this.useRoleForBindingName}
onChange={v => this.useRoleForBindingName = v}
/>
{
!this.useRoleForBindingName && (
<Input
autoFocus
placeholder="Name"
disabled={this.isEditing}
value={this.bindingName}
onChange={v => this.bindingName = v}
/>
)
}
</>
)
}
<SubTitle title="Binding targets"/>
<Select
isMulti
themeName="light"
placeholder="Select service accounts"
autoConvertOptions={false}
options={this.serviceAccountOptions}
onChange={(opts: BindingSelectOption[]) => {
if (!opts) opts = [];
this.selectedAccounts.replace(unwrapBindings(opts));
}}
maxMenuHeight={200}
/>
</>
);
}
render() {
const { ...dialogProps } = this.props;
const { isEditing, roleBinding, selectedRole, selectedBindings } = this;
const roleBindingName = roleBinding ? roleBinding.getName() : "";
const header = (
<h5>
{roleBindingName
? <>Edit RoleBinding <span className="name">{roleBindingName}</span></>
: "Add RoleBinding"
}
</h5>
);
const disableNext = this.isLoading || !selectedRole || !selectedBindings.length;
const nextLabel = isEditing ? "Update" : "Create";
return (
<Dialog
{...dialogProps}
className="AddRoleBindingDialog"
isOpen={dialogState.isOpen}
onOpen={this.onOpen}
close={this.close}
>
<Wizard header={header} done={this.close}>
<WizardStep
nextLabel={nextLabel} next={this.createBindings}
disabledNext={disableNext}
loading={this.isLoading}
>
{this.renderContents()}
</WizardStep>
</Wizard>
</Dialog>
);
}
}