mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-10 15:35:49 +01:00
added view and edit of admin user and groups
This commit is contained in:
@@ -26,8 +26,17 @@
|
|||||||
"force-base-url": "Force Base URL"
|
"force-base-url": "Force Base URL"
|
||||||
},
|
},
|
||||||
"admin-settings": {
|
"admin-settings": {
|
||||||
|
"name": "Administration Settings",
|
||||||
"admin-groups": "Admin Groups",
|
"admin-groups": "Admin Groups",
|
||||||
"admin-user": "Admin Users"
|
"admin-users": "Admin Users",
|
||||||
|
"remove-group-button": "Remove Admin Group",
|
||||||
|
"remove-user-button": "Remove Admin User",
|
||||||
|
"add-group-error": "The group name you want to add is not valid",
|
||||||
|
"add-group-textfield": "Add group you want to add to admin groups here",
|
||||||
|
"add-group-button": "Add Admin Group",
|
||||||
|
"add-user-error": "The user name you want to add is not valid",
|
||||||
|
"add-user-textfield": "Add user you want to add to admin users here",
|
||||||
|
"add-user-button": "Add Admin User"
|
||||||
},
|
},
|
||||||
"general-settings": {
|
"general-settings": {
|
||||||
"realm-description": "Realm Description",
|
"realm-description": "Realm Description",
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { DeleteButton } from "../../../components/buttons";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
t: string => string,
|
||||||
|
groupname: string,
|
||||||
|
removeGroup: string => void
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveAdminGroupButton extends React.Component<Props, State> {
|
||||||
|
render() {
|
||||||
|
const { t , groupname, removeGroup} = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classNames("is-pulled-right")}>
|
||||||
|
<DeleteButton
|
||||||
|
label={t("admin-settings.remove-group-button")}
|
||||||
|
action={(event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
removeGroup(groupname);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(RemoveAdminGroupButton);
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { DeleteButton } from "../../../components/buttons";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
t: string => string,
|
||||||
|
username: string,
|
||||||
|
removeUser: string => void
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveAdminUserButton extends React.Component<Props, State> {
|
||||||
|
render() {
|
||||||
|
const { t , username, removeUser} = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classNames("is-pulled-right")}>
|
||||||
|
<DeleteButton
|
||||||
|
label={t("admin-settings.remove-user-button")}
|
||||||
|
action={(event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
removeUser(username);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(RemoveAdminUserButton);
|
||||||
71
scm-ui/src/config/components/fields/AddAdminGroupField.js
Normal file
71
scm-ui/src/config/components/fields/AddAdminGroupField.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { AddButton } from "../../../components/buttons";
|
||||||
|
import InputField from "../../../components/forms/InputField";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
t: string => string,
|
||||||
|
addGroup: string => void
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
groupToAdd: string,
|
||||||
|
//validationError: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class AddAdminGroupField extends React.Component<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
groupToAdd: "",
|
||||||
|
//validationError: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
return (
|
||||||
|
<div className="field">
|
||||||
|
<InputField
|
||||||
|
|
||||||
|
label={t("admin-settings.add-group-textfield")}
|
||||||
|
errorMessage={t("admin-settings.add-group-error")}
|
||||||
|
onChange={this.handleAddGroupChange}
|
||||||
|
validationError={false}
|
||||||
|
value={this.state.groupToAdd}
|
||||||
|
onReturnPressed={this.appendGroup}
|
||||||
|
/>
|
||||||
|
<AddButton
|
||||||
|
label={t("admin-settings.add-group-button")}
|
||||||
|
action={this.addButtonClicked}
|
||||||
|
//disabled={!isMemberNameValid(this.state.memberToAdd)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addButtonClicked = (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.appendGroup();
|
||||||
|
};
|
||||||
|
|
||||||
|
appendGroup = () => {
|
||||||
|
const { groupToAdd } = this.state;
|
||||||
|
//if (isMemberNameValid(memberToAdd)) {
|
||||||
|
this.props.addGroup(groupToAdd);
|
||||||
|
this.setState({ ...this.state, groupToAdd: "" });
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
handleAddGroupChange = (groupname: string) => {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
groupToAdd: groupname,
|
||||||
|
//validationError: membername.length > 0 && !isMemberNameValid(membername)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(AddAdminGroupField);
|
||||||
71
scm-ui/src/config/components/fields/AddAdminUserField.js
Normal file
71
scm-ui/src/config/components/fields/AddAdminUserField.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import { AddButton } from "../../../components/buttons";
|
||||||
|
import InputField from "../../../components/forms/InputField";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
t: string => string,
|
||||||
|
addUser: string => void
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
userToAdd: string,
|
||||||
|
//validationError: boolean
|
||||||
|
};
|
||||||
|
|
||||||
|
class AddAdminUserField extends React.Component<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
userToAdd: "",
|
||||||
|
//validationError: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
return (
|
||||||
|
<div className="field">
|
||||||
|
<InputField
|
||||||
|
|
||||||
|
label={t("admin-settings.add-user-textfield")}
|
||||||
|
errorMessage={t("admin-settings.add-user-error")}
|
||||||
|
onChange={this.handleAddUserChange}
|
||||||
|
validationError={false}
|
||||||
|
value={this.state.userToAdd}
|
||||||
|
onReturnPressed={this.appendUser}
|
||||||
|
/>
|
||||||
|
<AddButton
|
||||||
|
label={t("admin-settings.add-user-button")}
|
||||||
|
action={this.addButtonClicked}
|
||||||
|
//disabled={!isMemberNameValid(this.state.memberToAdd)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
addButtonClicked = (event: Event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
this.appendUser();
|
||||||
|
};
|
||||||
|
|
||||||
|
appendUser = () => {
|
||||||
|
const { userToAdd } = this.state;
|
||||||
|
//if (isMemberNameValid(memberToAdd)) {
|
||||||
|
this.props.addUser(userToAdd);
|
||||||
|
this.setState({ ...this.state, userToAdd: "" });
|
||||||
|
// }
|
||||||
|
};
|
||||||
|
|
||||||
|
handleAddUserChange = (username: string) => {
|
||||||
|
this.setState({
|
||||||
|
...this.state,
|
||||||
|
userToAdd: username,
|
||||||
|
//validationError: membername.length > 0 && !isMemberNameValid(membername)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(AddAdminUserField);
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
// @flow
|
// @flow
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { translate } from "react-i18next";
|
import { translate } from "react-i18next";
|
||||||
import { Checkbox, InputField } from "../../../components/forms/index";
|
|
||||||
import Subtitle from "../../../components/layout/Subtitle";
|
import Subtitle from "../../../components/layout/Subtitle";
|
||||||
|
import AdminGroupTable from "../table/AdminGroupTable";
|
||||||
|
import ProxySettings from "./ProxySettings";
|
||||||
|
import AdminUserTable from "../table/AdminUserTable";
|
||||||
|
import AddAdminGroupField from "../fields/AddAdminGroupField";
|
||||||
|
import AddAdminUserField from "../fields/AddAdminUserField";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
adminGroups: string[],
|
adminGroups: string[],
|
||||||
@@ -13,17 +17,58 @@ type Props = {
|
|||||||
//TODO: Einbauen!
|
//TODO: Einbauen!
|
||||||
class AdminSettings extends React.Component<Props> {
|
class AdminSettings extends React.Component<Props> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { t, adminGroups, adminUsers } = this.props;
|
||||||
t,
|
|
||||||
adminGroups,
|
|
||||||
adminUsers
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
null
|
<div>
|
||||||
|
<Subtitle subtitle={t("admin-settings.name")} />
|
||||||
|
<AdminGroupTable
|
||||||
|
adminGroups={adminGroups}
|
||||||
|
onChange={(isValid, changedValue, name) =>
|
||||||
|
this.props.onChange(isValid, changedValue, name)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AddAdminGroupField addGroup={this.addGroup} />
|
||||||
|
<AdminUserTable
|
||||||
|
adminUsers={adminUsers}
|
||||||
|
onChange={(isValid, changedValue, name) =>
|
||||||
|
this.props.onChange(isValid, changedValue, name)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AddAdminUserField addUser={this.addUser} />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addGroup = (groupname: string) => {
|
||||||
|
if (this.isAdminGroupMember(groupname)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.onChange(
|
||||||
|
true,
|
||||||
|
[...this.props.adminGroups, groupname],
|
||||||
|
"adminGroups"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
isAdminGroupMember = (groupname: string) => {
|
||||||
|
return this.props.adminGroups.includes(groupname);
|
||||||
|
};
|
||||||
|
|
||||||
|
addUser = (username: string) => {
|
||||||
|
if (this.isAdminUserMember(username)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.onChange(
|
||||||
|
true,
|
||||||
|
[...this.props.adminUsers, username],
|
||||||
|
"adminUsers"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
isAdminUserMember = (username: string) => {
|
||||||
|
return this.props.adminUsers.includes(username);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default translate("config")(AdminSettings);
|
export default translate("config")(AdminSettings);
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import type { Config } from "../../types/Config";
|
|||||||
import ProxySettings from "./ProxySettings";
|
import ProxySettings from "./ProxySettings";
|
||||||
import GeneralSettings from "./GeneralSettings";
|
import GeneralSettings from "./GeneralSettings";
|
||||||
import BaseUrlSettings from "./BaseUrlSettings";
|
import BaseUrlSettings from "./BaseUrlSettings";
|
||||||
|
import AdminSettings from "./AdminSettings";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
submitForm: Config => void,
|
submitForm: Config => void,
|
||||||
config?: Config,
|
config?: Config,
|
||||||
loading?: boolean,
|
loading?: boolean,
|
||||||
t: string => string
|
t: string => string,
|
||||||
|
configUpdatePermission: boolean
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@@ -85,6 +87,7 @@ class ConfigForm extends React.Component<Props, State> {
|
|||||||
this.onChange(isValid, changedValue, name)
|
this.onChange(isValid, changedValue, name)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<hr />
|
||||||
<BaseUrlSettings
|
<BaseUrlSettings
|
||||||
baseUrl={config.baseUrl}
|
baseUrl={config.baseUrl}
|
||||||
forceBaseUrl={config.forceBaseUrl}
|
forceBaseUrl={config.forceBaseUrl}
|
||||||
@@ -92,6 +95,15 @@ class ConfigForm extends React.Component<Props, State> {
|
|||||||
this.onChange(isValid, changedValue, name)
|
this.onChange(isValid, changedValue, name)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<hr />
|
||||||
|
<AdminSettings
|
||||||
|
adminGroups={config.adminGroups}
|
||||||
|
adminUsers={config.adminUsers}
|
||||||
|
onChange={(isValid, changedValue, name) =>
|
||||||
|
this.onChange(isValid, changedValue, name)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<hr />
|
||||||
<ProxySettings
|
<ProxySettings
|
||||||
proxyPassword={config.proxyPassword ? config.proxyPassword : ""}
|
proxyPassword={config.proxyPassword ? config.proxyPassword : ""}
|
||||||
proxyPort={config.proxyPort}
|
proxyPort={config.proxyPort}
|
||||||
@@ -102,6 +114,7 @@ class ConfigForm extends React.Component<Props, State> {
|
|||||||
this.onChange(isValid, changedValue, name)
|
this.onChange(isValid, changedValue, name)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<hr />
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
// disabled={!this.isValid()}
|
// disabled={!this.isValid()}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
|||||||
47
scm-ui/src/config/components/table/AdminGroupTable.js
Normal file
47
scm-ui/src/config/components/table/AdminGroupTable.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import RemoveAdminGroupButton from "../buttons/RemoveAdminGroupButton";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
adminGroups: string[],
|
||||||
|
t: string => string,
|
||||||
|
onChange: (boolean, any, string) => void
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {};
|
||||||
|
|
||||||
|
class AdminGroupTable extends React.Component<Props, State> {
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="label">{t("admin-settings.admin-groups")}</label>
|
||||||
|
<table className="table is-hoverable is-fullwidth">
|
||||||
|
<tbody>
|
||||||
|
{this.props.adminGroups.map(group => {
|
||||||
|
return (
|
||||||
|
<tr key={group}>
|
||||||
|
<td key={group}>{group}</td>
|
||||||
|
<td>
|
||||||
|
<RemoveAdminGroupButton
|
||||||
|
groupname={group}
|
||||||
|
removeGroup={this.removeGroup}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeGroup = (groupname: string) => {
|
||||||
|
const newGroups = this.props.adminGroups.filter(name => name !== groupname);
|
||||||
|
this.props.onChange(true, newGroups, "adminGroups");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(AdminGroupTable);
|
||||||
47
scm-ui/src/config/components/table/AdminUserTable.js
Normal file
47
scm-ui/src/config/components/table/AdminUserTable.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//@flow
|
||||||
|
import React from "react";
|
||||||
|
import { translate } from "react-i18next";
|
||||||
|
import RemoveAdminUserButton from "../buttons/RemoveAdminUserButton";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
adminUsers: string[],
|
||||||
|
t: string => string,
|
||||||
|
onChange: (boolean, any, string) => void
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {};
|
||||||
|
|
||||||
|
class AdminUserTable extends React.Component<Props, State> {
|
||||||
|
render() {
|
||||||
|
const { t } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<label className="label">{t("admin-settings.admin-users")}</label>
|
||||||
|
<table className="table is-hoverable is-fullwidth">
|
||||||
|
<tbody>
|
||||||
|
{this.props.adminUsers.map(user => {
|
||||||
|
return (
|
||||||
|
<tr key={user}>
|
||||||
|
<td key={user}>{user}</td>
|
||||||
|
<td>
|
||||||
|
<RemoveAdminUserButton
|
||||||
|
username={user}
|
||||||
|
removeUser={this.removeUser}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeUser = (username: string) => {
|
||||||
|
const newUsers = this.props.adminUsers.filter(name => name !== username);
|
||||||
|
this.props.onChange(true, newUsers, "adminUsers");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default translate("config")(AdminUserTable);
|
||||||
@@ -7,7 +7,8 @@ import {
|
|||||||
isFetchConfigPending,
|
isFetchConfigPending,
|
||||||
getConfig,
|
getConfig,
|
||||||
modifyConfig,
|
modifyConfig,
|
||||||
isModifyConfigPending
|
isModifyConfigPending,
|
||||||
|
getConfigUpdatePermission
|
||||||
} from "../modules/config";
|
} from "../modules/config";
|
||||||
import connect from "react-redux/es/connect/connect";
|
import connect from "react-redux/es/connect/connect";
|
||||||
import ErrorPage from "../../components/ErrorPage";
|
import ErrorPage from "../../components/ErrorPage";
|
||||||
@@ -21,6 +22,7 @@ type Props = {
|
|||||||
loading: boolean,
|
loading: boolean,
|
||||||
error: Error,
|
error: Error,
|
||||||
config: Config,
|
config: Config,
|
||||||
|
configUpdatePermission: boolean,
|
||||||
// dispatch functions
|
// dispatch functions
|
||||||
modifyConfig: (config: User, callback?: () => void) => void,
|
modifyConfig: (config: User, callback?: () => void) => void,
|
||||||
// context objects
|
// context objects
|
||||||
@@ -31,6 +33,7 @@ type Props = {
|
|||||||
|
|
||||||
class GlobalConfig extends React.Component<Props> {
|
class GlobalConfig extends React.Component<Props> {
|
||||||
configModified = (config: Config) => () => {
|
configModified = (config: Config) => () => {
|
||||||
|
this.props.fetchConfig();
|
||||||
this.props.history.push(`/config`);
|
this.props.history.push(`/config`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,11 +42,12 @@ class GlobalConfig extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
modifyConfig = (config: Config) => {
|
modifyConfig = (config: Config) => {
|
||||||
|
console.log(config);
|
||||||
this.props.modifyConfig(config, this.configModified(config));
|
this.props.modifyConfig(config, this.configModified(config));
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { t, error, loading, config } = this.props;
|
const { t, error, loading, config, configUpdatePermission } = this.props;
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
@@ -51,6 +55,7 @@ class GlobalConfig extends React.Component<Props> {
|
|||||||
title={t("global-config.error-title")}
|
title={t("global-config.error-title")}
|
||||||
subtitle={t("global-config.error-subtitle")}
|
subtitle={t("global-config.error-subtitle")}
|
||||||
error={error}
|
error={error}
|
||||||
|
configUpdatePermission={configUpdatePermission}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -86,11 +91,13 @@ const mapStateToProps = state => {
|
|||||||
const loading = isFetchConfigPending(state) || isModifyConfigPending(state); //TODO: Button lädt so nicht, sondern gesamte Seite
|
const loading = isFetchConfigPending(state) || isModifyConfigPending(state); //TODO: Button lädt so nicht, sondern gesamte Seite
|
||||||
const error = getFetchConfigFailure(state);
|
const error = getFetchConfigFailure(state);
|
||||||
const config = getConfig(state);
|
const config = getConfig(state);
|
||||||
|
const configUpdatePermission = getConfigUpdatePermission(state);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
config
|
config,
|
||||||
|
configUpdatePermission
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user