mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
1 parent
e810eab39f
commit
b7974827d2
786 files changed
+14567
-24066
No files matched your search
@@ -0,0 +1,11 @@
|
||||
.AddRoleBindingDialog {
|
||||
.Select + .Select {
|
||||
margin-top: $margin /2;
|
||||
}
|
||||
.Checkbox {
|
||||
margin-top: $margin;
|
||||
}
|
||||
.name {
|
||||
color: #a0a0a0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import "./add-role-binding-dialog.scss"
|
||||
|
||||
import React from "react";
|
||||
import { computed, observable } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { Dialog, DialogProps } from "../dialog";
|
||||
import { Wizard, WizardStep } from "../wizard";
|
||||
import { Select, SelectOption } from "../select";
|
||||
import { SubTitle } from "../layout/sub-title";
|
||||
import { IRoleBindingSubject, RoleBinding, ServiceAccount, Role } 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 { showDetails } from "../../navigation";
|
||||
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";
|
||||
|
||||
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> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class AddRoleBindingDialog extends React.Component<Props> {
|
||||
@observable static isOpen = false;
|
||||
@observable static data: RoleBinding = null;
|
||||
|
||||
static open(roleBinding?: RoleBinding) {
|
||||
AddRoleBindingDialog.isOpen = true;
|
||||
AddRoleBindingDialog.data = roleBinding;
|
||||
}
|
||||
|
||||
static close() {
|
||||
AddRoleBindingDialog.isOpen = false;
|
||||
}
|
||||
|
||||
get roleBinding(): RoleBinding {
|
||||
return AddRoleBindingDialog.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 = [
|
||||
namespaceStore,
|
||||
rolesStore,
|
||||
serviceAccountsStore,
|
||||
];
|
||||
this.isLoading = true;
|
||||
await Promise.all(stores.map(store => store.loadAll()));
|
||||
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: 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={<Trans>Context</Trans>}/>
|
||||
<NamespaceSelect
|
||||
showClusterOption
|
||||
themeName="light"
|
||||
isDisabled={this.isEditing}
|
||||
value={this.bindContext}
|
||||
onChange={({ value }) => this.onBindContextChange(value)}
|
||||
/>
|
||||
|
||||
<SubTitle title={<Trans>Role</Trans>}/>
|
||||
<Select
|
||||
key={this.selectedRoleId}
|
||||
themeName="light"
|
||||
placeholder={_i18n._(t`Select role..`)}
|
||||
isDisabled={this.isEditing}
|
||||
options={this.roleOptions}
|
||||
value={this.selectedRoleId}
|
||||
onChange={({ value }) => this.selectedRoleId = value}
|
||||
/>
|
||||
{
|
||||
!this.isEditing && (
|
||||
<>
|
||||
<Checkbox
|
||||
theme="light"
|
||||
label={<Trans>Use same name for RoleBinding</Trans>}
|
||||
value={this.useRoleForBindingName}
|
||||
onChange={v => this.useRoleForBindingName = v}
|
||||
/>
|
||||
{
|
||||
!this.useRoleForBindingName && (
|
||||
<Input
|
||||
autoFocus
|
||||
placeholder={_i18n._(t`Name`)}
|
||||
disabled={this.isEditing}
|
||||
value={this.bindingName}
|
||||
onChange={v => this.bindingName = v}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
<SubTitle title={<Trans>Binding targets</Trans>}/>
|
||||
<Select
|
||||
isMulti
|
||||
themeName="light"
|
||||
placeholder={_i18n._(t`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
|
||||
? <Trans>Edit RoleBinding <span className="name">{roleBindingName}</span></Trans>
|
||||
: <Trans>Add RoleBinding</Trans>
|
||||
}
|
||||
</h5>
|
||||
);
|
||||
const disableNext = this.isLoading || !selectedRole || !selectedBindings.length;
|
||||
const nextLabel = isEditing ? <Trans>Update</Trans> : <Trans>Create</Trans>;
|
||||
return (
|
||||
<Dialog
|
||||
{...dialogProps}
|
||||
className="AddRoleBindingDialog"
|
||||
isOpen={AddRoleBindingDialog.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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from "./role-bindings"
|
||||
export * from "./role-binding-details"
|
||||
export * from "./add-role-binding-dialog"
|
||||
@@ -0,0 +1,2 @@
|
||||
.RoleBindingDetails {
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import "./role-binding-details.scss"
|
||||
|
||||
import React from "react";
|
||||
import { t, Trans } from "@lingui/macro";
|
||||
import { AddRemoveButtons } from "../add-remove-buttons";
|
||||
import { clusterRoleBindingApi, IRoleBindingSubject, RoleBinding, roleBindingApi } from "../../api/endpoints";
|
||||
import { autobind, prevDefault } from "../../utils";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../table";
|
||||
import { ConfirmDialog } from "../confirm-dialog";
|
||||
import { DrawerTitle } from "../drawer";
|
||||
import { KubeEventDetails } from "../+events/kube-event-details";
|
||||
import { disposeOnUnmount, observer } from "mobx-react";
|
||||
import { observable, reaction } from "mobx";
|
||||
import { roleBindingsStore } from "./role-bindings.store";
|
||||
import { AddRoleBindingDialog } from "./add-role-binding-dialog";
|
||||
import { KubeObjectDetailsProps } from "../kube-object";
|
||||
import { _i18n } from "../../i18n";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
import { KubeObjectMeta } from "../kube-object/kube-object-meta";
|
||||
|
||||
interface Props extends KubeObjectDetailsProps<RoleBinding> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class RoleBindingDetails extends React.Component<Props> {
|
||||
@observable selectedSubjects = observable.array<IRoleBindingSubject>([], { deep: false });
|
||||
|
||||
async componentDidMount() {
|
||||
disposeOnUnmount(this, [
|
||||
reaction(() => this.props.object, (obj) => {
|
||||
this.selectedSubjects.clear();
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
selectSubject(subject: IRoleBindingSubject) {
|
||||
const { selectedSubjects } = this;
|
||||
const isSelected = selectedSubjects.includes(subject);
|
||||
selectedSubjects.replace(
|
||||
isSelected
|
||||
? selectedSubjects.filter(sub => sub !== subject) // unselect
|
||||
: selectedSubjects.concat(subject) // select
|
||||
)
|
||||
}
|
||||
|
||||
@autobind()
|
||||
removeSelectedSubjects() {
|
||||
const { object: roleBinding } = this.props;
|
||||
const { selectedSubjects } = this;
|
||||
ConfirmDialog.open({
|
||||
ok: () => roleBindingsStore.updateSubjects({ roleBinding, removeSubjects: selectedSubjects }),
|
||||
labelOk: _i18n._(t`Remove`),
|
||||
message: (
|
||||
<p><Trans>Remove selected bindings for <b>{roleBinding.getName()}</b>?</Trans></p>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
const { selectedSubjects } = this;
|
||||
const { object: roleBinding } = this.props;
|
||||
if (!roleBinding) {
|
||||
return null;
|
||||
}
|
||||
const name = roleBinding.getName();
|
||||
const { roleRef } = roleBinding;
|
||||
return (
|
||||
<div className="RoleBindingDetails">
|
||||
<KubeObjectMeta object={roleBinding}/>
|
||||
|
||||
<DrawerTitle title={<Trans>Reference</Trans>}/>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableCell><Trans>Kind</Trans></TableCell>
|
||||
<TableCell><Trans>Name</Trans></TableCell>
|
||||
<TableCell><Trans>API Group</Trans></TableCell>
|
||||
</TableHead>
|
||||
<TableRow>
|
||||
<TableCell>{roleRef.kind}</TableCell>
|
||||
<TableCell>{roleRef.name}</TableCell>
|
||||
<TableCell>{roleRef.apiGroup}</TableCell>
|
||||
</TableRow>
|
||||
</Table>
|
||||
|
||||
<DrawerTitle title={<Trans>Bindings</Trans>}/>
|
||||
<Table selectable className="bindings box grow">
|
||||
<TableHead>
|
||||
<TableCell checkbox/>
|
||||
<TableCell className="binding"><Trans>Binding</Trans></TableCell>
|
||||
<TableCell className="type"><Trans>Type</Trans></TableCell>
|
||||
<TableCell className="type"><Trans>Namespace</Trans></TableCell>
|
||||
</TableHead>
|
||||
{
|
||||
roleBinding.getSubjects().map((subject, i) => {
|
||||
const { kind, name, namespace } = subject;
|
||||
const isSelected = selectedSubjects.includes(subject);
|
||||
return (
|
||||
<TableRow
|
||||
key={i} selected={isSelected}
|
||||
onClick={prevDefault(() => this.selectSubject(subject))}
|
||||
>
|
||||
<TableCell checkbox isChecked={isSelected}/>
|
||||
<TableCell className="binding">{name}</TableCell>
|
||||
<TableCell className="type">{kind}</TableCell>
|
||||
<TableCell className="ns">{namespace || "-"}</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})
|
||||
}
|
||||
</Table>
|
||||
|
||||
<KubeEventDetails object={roleBinding}/>
|
||||
|
||||
<AddRemoveButtons
|
||||
onAdd={() => AddRoleBindingDialog.open(roleBinding)}
|
||||
onRemove={selectedSubjects.length ? this.removeSelectedSubjects : null}
|
||||
addTooltip={<Trans>Add bindings to {name}</Trans>}
|
||||
removeTooltip={<Trans>Remove selected bindings from ${name}</Trans>}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
apiManager.registerViews([roleBindingApi, clusterRoleBindingApi], {
|
||||
Details: RoleBindingDetails,
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
.RoleBindings {
|
||||
.help-icon {
|
||||
margin-left: $margin / 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import difference from "lodash/difference"
|
||||
import uniqBy from "lodash/uniqBy"
|
||||
import { clusterRoleBindingApi, IRoleBindingSubject, RoleBinding, roleBindingApi } from "../../api/endpoints";
|
||||
import { KubeObjectStore } from "../../kube-object.store";
|
||||
import { autobind } from "../../utils";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
@autobind()
|
||||
export class RoleBindingsStore extends KubeObjectStore<RoleBinding> {
|
||||
api = clusterRoleBindingApi
|
||||
|
||||
subscribe() {
|
||||
return super.subscribe([clusterRoleBindingApi, roleBindingApi])
|
||||
}
|
||||
|
||||
protected sortItems(items: RoleBinding[]) {
|
||||
return super.sortItems(items, [
|
||||
roleBinding => roleBinding.kind,
|
||||
roleBinding => roleBinding.getName()
|
||||
])
|
||||
}
|
||||
|
||||
protected loadItem(params: { name: string; namespace?: string }) {
|
||||
if (params.namespace) return roleBindingApi.get(params)
|
||||
return clusterRoleBindingApi.get(params)
|
||||
}
|
||||
|
||||
protected loadItems(namespaces?: string[]) {
|
||||
if (namespaces) {
|
||||
return Promise.all(
|
||||
namespaces.map(namespace => roleBindingApi.list({ namespace }))
|
||||
).then(items => items.flat())
|
||||
}
|
||||
else {
|
||||
return Promise.all([clusterRoleBindingApi.list(), roleBindingApi.list()])
|
||||
.then(items => items.flat())
|
||||
}
|
||||
}
|
||||
|
||||
protected async createItem(params: { name: string; namespace?: string }, data?: Partial<RoleBinding>) {
|
||||
if (params.namespace) {
|
||||
return roleBindingApi.create(params, data)
|
||||
}
|
||||
else {
|
||||
return clusterRoleBindingApi.create(params, data)
|
||||
}
|
||||
}
|
||||
|
||||
async updateSubjects(params: {
|
||||
roleBinding: RoleBinding;
|
||||
addSubjects?: IRoleBindingSubject[];
|
||||
removeSubjects?: IRoleBindingSubject[];
|
||||
}) {
|
||||
const { roleBinding, addSubjects, removeSubjects } = params;
|
||||
const currentSubjects = roleBinding.getSubjects();
|
||||
let newSubjects = currentSubjects;
|
||||
if (addSubjects) {
|
||||
newSubjects = uniqBy(currentSubjects.concat(addSubjects), ({ kind, name, namespace }) => {
|
||||
return [kind, name, namespace].join("-");
|
||||
})
|
||||
}
|
||||
else if (removeSubjects) {
|
||||
newSubjects = difference(currentSubjects, removeSubjects);
|
||||
}
|
||||
return this.update(roleBinding, {
|
||||
roleRef: roleBinding.roleRef,
|
||||
subjects: newSubjects
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const roleBindingsStore = new RoleBindingsStore();
|
||||
|
||||
apiManager.registerStore(roleBindingApi, roleBindingsStore);
|
||||
apiManager.registerStore(clusterRoleBindingApi, roleBindingsStore);
|
||||
@@ -0,0 +1,77 @@
|
||||
import "./role-bindings.scss"
|
||||
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Trans } from "@lingui/macro";
|
||||
import { RouteComponentProps } from "react-router";
|
||||
import { Icon } from "../icon";
|
||||
import { IRoleBindingsRouteParams } from "../+user-management/user-management.routes";
|
||||
import { KubeObjectMenu, KubeObjectMenuProps } from "../kube-object/kube-object-menu";
|
||||
import { clusterRoleBindingApi, RoleBinding, roleBindingApi } from "../../api/endpoints";
|
||||
import { roleBindingsStore } from "./role-bindings.store";
|
||||
import { KubeObjectListLayout } from "../kube-object";
|
||||
import { AddRoleBindingDialog } from "./add-role-binding-dialog";
|
||||
import { KubeObject } from "../../api/kube-object";
|
||||
import { apiManager } from "../../api/api-manager";
|
||||
|
||||
enum sortBy {
|
||||
name = "name",
|
||||
namespace = "namespace",
|
||||
bindings = "bindings",
|
||||
age = "age",
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps<IRoleBindingsRouteParams> {
|
||||
}
|
||||
|
||||
@observer
|
||||
export class RoleBindings extends React.Component<Props> {
|
||||
render() {
|
||||
return (
|
||||
<KubeObjectListLayout
|
||||
className="RoleBindings"
|
||||
store={roleBindingsStore}
|
||||
sortingCallbacks={{
|
||||
[sortBy.name]: (binding: RoleBinding) => binding.getName(),
|
||||
[sortBy.namespace]: (binding: RoleBinding) => binding.getNs(),
|
||||
[sortBy.bindings]: (binding: RoleBinding) => binding.getSubjectNames(),
|
||||
[sortBy.age]: (binding: RoleBinding) => binding.metadata.creationTimestamp,
|
||||
}}
|
||||
searchFilters={[
|
||||
(binding: RoleBinding) => binding.getSearchFields(),
|
||||
(binding: RoleBinding) => binding.getSubjectNames(),
|
||||
]}
|
||||
renderHeaderTitle={<Trans>Role Bindings</Trans>}
|
||||
renderTableHeader={[
|
||||
{ title: <Trans>Name</Trans>, className: "name", sortBy: sortBy.name },
|
||||
{ title: <Trans>Bindings</Trans>, className: "bindings", sortBy: sortBy.bindings },
|
||||
{ title: <Trans>Namespace</Trans>, className: "namespace", sortBy: sortBy.namespace },
|
||||
{ title: <Trans>Age</Trans>, className: "age", sortBy: sortBy.age },
|
||||
]}
|
||||
renderTableContents={(binding: RoleBinding) => [
|
||||
binding.getName(),
|
||||
binding.getSubjectNames(),
|
||||
binding.getNs() || "-",
|
||||
binding.getAge(),
|
||||
]}
|
||||
renderItemMenu={(item: RoleBinding) => {
|
||||
return <RoleBindingMenu object={item}/>
|
||||
}}
|
||||
addRemoveButtons={{
|
||||
onAdd: () => AddRoleBindingDialog.open(),
|
||||
addTooltip: <Trans>Create new RoleBinding</Trans>,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function RoleBindingMenu(props: KubeObjectMenuProps<RoleBinding>) {
|
||||
return (
|
||||
<KubeObjectMenu {...props}/>
|
||||
)
|
||||
}
|
||||
|
||||
apiManager.registerViews([roleBindingApi, clusterRoleBindingApi], {
|
||||
Menu: RoleBindingMenu,
|
||||
})
|
||||
Reference in new issue
Block a user