mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-16 10:16:16 +01:00
merge with 2.0.0-m3 branch
This commit is contained in:
33
scm-ui/src/components/buttons/RemoveEntryOfTableButton.js
Normal file
33
scm-ui/src/components/buttons/RemoveEntryOfTableButton.js
Normal file
@@ -0,0 +1,33 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { DeleteButton } from ".";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
entryname: string,
|
||||
removeEntry: string => void,
|
||||
disabled: boolean,
|
||||
label: string
|
||||
};
|
||||
|
||||
type State = {};
|
||||
|
||||
class RemoveEntryOfTableButton extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { label, entryname, removeEntry, disabled } = this.props;
|
||||
return (
|
||||
<div className={classNames("is-pulled-right")}>
|
||||
<DeleteButton
|
||||
label={label}
|
||||
action={(event: Event) => {
|
||||
event.preventDefault();
|
||||
removeEntry(entryname);
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default RemoveEntryOfTableButton;
|
||||
@@ -4,3 +4,4 @@ export { default as CreateButton } from "./CreateButton";
|
||||
export { default as DeleteButton } from "./DeleteButton";
|
||||
export { default as EditButton } from "./EditButton";
|
||||
export { default as SubmitButton } from "./SubmitButton";
|
||||
export {default as RemoveEntryOfTableButton} from "./RemoveEntryOfTableButton";
|
||||
|
||||
68
scm-ui/src/components/forms/AddEntryToTableField.js
Normal file
68
scm-ui/src/components/forms/AddEntryToTableField.js
Normal file
@@ -0,0 +1,68 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
|
||||
import { AddButton } from "../buttons";
|
||||
import InputField from "./InputField";
|
||||
|
||||
type Props = {
|
||||
addEntry: string => void,
|
||||
disabled: boolean,
|
||||
buttonLabel: string,
|
||||
fieldLabel: string,
|
||||
errorMessage: string
|
||||
};
|
||||
|
||||
type State = {
|
||||
entryToAdd: string
|
||||
};
|
||||
|
||||
class AddEntryToTableField extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
entryToAdd: ""
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { disabled, buttonLabel, fieldLabel, errorMessage } = this.props;
|
||||
return (
|
||||
<div className="field">
|
||||
<InputField
|
||||
label={fieldLabel}
|
||||
errorMessage={errorMessage}
|
||||
onChange={this.handleAddEntryChange}
|
||||
validationError={false}
|
||||
value={this.state.entryToAdd}
|
||||
onReturnPressed={this.appendEntry}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<AddButton
|
||||
label={buttonLabel}
|
||||
action={this.addButtonClicked}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
addButtonClicked = (event: Event) => {
|
||||
event.preventDefault();
|
||||
this.appendEntry();
|
||||
};
|
||||
|
||||
appendEntry = () => {
|
||||
const { entryToAdd } = this.state;
|
||||
this.props.addEntry(entryToAdd);
|
||||
this.setState({ ...this.state, entryToAdd: "" });
|
||||
};
|
||||
|
||||
handleAddEntryChange = (entryname: string) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
entryToAdd: entryname
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default AddEntryToTableField;
|
||||
@@ -4,7 +4,8 @@ import React from "react";
|
||||
type Props = {
|
||||
label?: string,
|
||||
checked: boolean,
|
||||
onChange?: boolean => void
|
||||
onChange?: boolean => void,
|
||||
disabled?: boolean
|
||||
};
|
||||
class Checkbox extends React.Component<Props> {
|
||||
onCheckboxChange = (event: SyntheticInputEvent<HTMLInputElement>) => {
|
||||
@@ -17,11 +18,12 @@ class Checkbox extends React.Component<Props> {
|
||||
return (
|
||||
<div className="field">
|
||||
<div className="control">
|
||||
<label className="checkbox">
|
||||
<label className="checkbox" disabled={this.props.disabled}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={this.props.checked}
|
||||
onChange={this.onCheckboxChange}
|
||||
disabled={this.props.disabled}
|
||||
/>
|
||||
{this.props.label}
|
||||
</label>
|
||||
|
||||
@@ -11,7 +11,8 @@ type Props = {
|
||||
onChange: string => void,
|
||||
onReturnPressed?: () => void,
|
||||
validationError: boolean,
|
||||
errorMessage: string
|
||||
errorMessage: string,
|
||||
disabled?: boolean
|
||||
};
|
||||
|
||||
class InputField extends React.Component<Props> {
|
||||
@@ -40,21 +41,33 @@ class InputField extends React.Component<Props> {
|
||||
return "";
|
||||
};
|
||||
|
||||
|
||||
handleKeyPress = (event: SyntheticKeyboardEvent<HTMLInputElement>) => {
|
||||
const onReturnPressed = this.props.onReturnPressed;
|
||||
if (!onReturnPressed) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
onReturnPressed();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { type, placeholder, value, validationError, errorMessage } = this.props;
|
||||
const {
|
||||
type,
|
||||
placeholder,
|
||||
value,
|
||||
validationError,
|
||||
errorMessage,
|
||||
disabled
|
||||
} = this.props;
|
||||
const errorView = validationError ? "is-danger" : "";
|
||||
const helper = validationError ? <p className="help is-danger">{errorMessage}</p> : "";
|
||||
const helper = validationError ? (
|
||||
<p className="help is-danger">{errorMessage}</p>
|
||||
) : (
|
||||
""
|
||||
);
|
||||
return (
|
||||
<div className="field">
|
||||
{this.renderLabel()}
|
||||
@@ -63,15 +76,13 @@ class InputField extends React.Component<Props> {
|
||||
ref={input => {
|
||||
this.field = input;
|
||||
}}
|
||||
className={ classNames(
|
||||
"input",
|
||||
errorView
|
||||
)}
|
||||
className={classNames("input", errorView)}
|
||||
type={type}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={this.handleInput}
|
||||
onKeyPress={this.handleKeyPress}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
{helper}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
import * as React from "react";
|
||||
import Loading from "./../Loading";
|
||||
import ErrorNotification from "./../ErrorNotification";
|
||||
import Title from "./Title";
|
||||
import Subtitle from "./Subtitle";
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
title?: string,
|
||||
subtitle?: string,
|
||||
loading?: boolean,
|
||||
error?: Error,
|
||||
@@ -14,12 +16,12 @@ type Props = {
|
||||
|
||||
class Page extends React.Component<Props> {
|
||||
render() {
|
||||
const { title, error } = this.props;
|
||||
const { title, error, subtitle } = this.props;
|
||||
return (
|
||||
<section className="section">
|
||||
<div className="container">
|
||||
<h1 className="title">{title}</h1>
|
||||
{this.renderSubtitle()}
|
||||
<Title title={title} />
|
||||
<Subtitle subtitle={subtitle} />
|
||||
<ErrorNotification error={error} />
|
||||
{this.renderContent()}
|
||||
</div>
|
||||
@@ -27,14 +29,6 @@ class Page extends React.Component<Props> {
|
||||
);
|
||||
}
|
||||
|
||||
renderSubtitle() {
|
||||
const { subtitle } = this.props;
|
||||
if (subtitle) {
|
||||
return <h2 className="subtitle">{subtitle}</h2>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { loading, children, showContentOnError, error } = this.props;
|
||||
if (error && !showContentOnError) {
|
||||
|
||||
17
scm-ui/src/components/layout/Subtitle.js
Normal file
17
scm-ui/src/components/layout/Subtitle.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
subtitle?: string
|
||||
};
|
||||
|
||||
class Subtitle extends React.Component<Props> {
|
||||
render() {
|
||||
const { subtitle } = this.props;
|
||||
if (subtitle) {
|
||||
return <h1 className="subtitle">{subtitle}</h1>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default Subtitle;
|
||||
17
scm-ui/src/components/layout/Title.js
Normal file
17
scm-ui/src/components/layout/Title.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from "react";
|
||||
|
||||
type Props = {
|
||||
title?: string
|
||||
};
|
||||
|
||||
class Title extends React.Component<Props> {
|
||||
render() {
|
||||
const { title } = this.props;
|
||||
if (title) {
|
||||
return <h1 className="title">{title}</h1>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default Title;
|
||||
@@ -28,6 +28,10 @@ class PrimaryNavigation extends React.Component<Props> {
|
||||
match="/(group|groups)"
|
||||
label={t("primary-navigation.groups")}
|
||||
/>
|
||||
<PrimaryNavigationLink
|
||||
to="/config"
|
||||
label={t("primary-navigation.config")}
|
||||
/>
|
||||
<PrimaryNavigationLink
|
||||
to="/logout"
|
||||
label={t("primary-navigation.logout")}
|
||||
|
||||
@@ -10,3 +10,7 @@ const mailRegex = /^[A-z0-9][\w.-]*@[A-z0-9][\w\-.]*\.[A-z0-9][A-z0-9-]+$/;
|
||||
export const isMailValid = (mail: string) => {
|
||||
return mailRegex.test(mail);
|
||||
};
|
||||
|
||||
export const isNumberValid = (number: string) => {
|
||||
return !isNaN(number);
|
||||
};
|
||||
|
||||
@@ -85,3 +85,18 @@ describe("test mail validation", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("test number validation", () => {
|
||||
it("should return false", () => {
|
||||
const invalid = ["1a", "35gu", "dj6", "45,5", "test"];
|
||||
for (let number of invalid) {
|
||||
expect(validator.isNumberValid(number)).toBe(false);
|
||||
}
|
||||
});
|
||||
it("should return true", () => {
|
||||
const valid = ["1", "35", "2", "235", "34.4"];
|
||||
for (let number of valid) {
|
||||
expect(validator.isNumberValid(number)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
87
scm-ui/src/config/components/form/AdminSettings.js
Normal file
87
scm-ui/src/config/components/form/AdminSettings.js
Normal file
@@ -0,0 +1,87 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import Subtitle from "../../../components/layout/Subtitle";
|
||||
import AdminGroupTable from "../table/AdminGroupTable";
|
||||
import AdminUserTable from "../table/AdminUserTable";
|
||||
import AddEntryToTableField from "../../../components/forms/AddEntryToTableField";
|
||||
|
||||
type Props = {
|
||||
adminGroups: string[],
|
||||
adminUsers: string[],
|
||||
t: string => string,
|
||||
onChange: (boolean, any, string) => void,
|
||||
hasUpdatePermission: boolean
|
||||
};
|
||||
|
||||
class AdminSettings extends React.Component<Props> {
|
||||
render() {
|
||||
const { t, adminGroups, adminUsers, hasUpdatePermission } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Subtitle subtitle={t("admin-settings.name")} />
|
||||
<AdminGroupTable
|
||||
adminGroups={adminGroups}
|
||||
onChange={(isValid, changedValue, name) =>
|
||||
this.props.onChange(isValid, changedValue, name)
|
||||
}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<AddEntryToTableField
|
||||
addEntry={this.addGroup}
|
||||
disabled={!hasUpdatePermission}
|
||||
buttonLabel={t("admin-settings.add-group-button")}
|
||||
fieldLabel={t("admin-settings.add-group-textfield")}
|
||||
errorMessage={t("admin-settings.add-group-error")}
|
||||
/>
|
||||
<AdminUserTable
|
||||
adminUsers={adminUsers}
|
||||
onChange={(isValid, changedValue, name) =>
|
||||
this.props.onChange(isValid, changedValue, name)
|
||||
}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<AddEntryToTableField
|
||||
addEntry={this.addUser}
|
||||
disabled={!hasUpdatePermission}
|
||||
buttonLabel={t("admin-settings.add-user-button")}
|
||||
fieldLabel={t("admin-settings.add-user-textfield")}
|
||||
errorMessage={t("admin-settings.add-user-error")}
|
||||
/>
|
||||
</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);
|
||||
46
scm-ui/src/config/components/form/BaseUrlSettings.js
Normal file
46
scm-ui/src/config/components/form/BaseUrlSettings.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Checkbox, InputField } from "../../../components/forms/index";
|
||||
import Subtitle from "../../../components/layout/Subtitle";
|
||||
|
||||
type Props = {
|
||||
baseUrl: string,
|
||||
forceBaseUrl: boolean,
|
||||
t: string => string,
|
||||
onChange: (boolean, any, string) => void,
|
||||
hasUpdatePermission: boolean
|
||||
};
|
||||
|
||||
class BaseUrlSettings extends React.Component<Props> {
|
||||
render() {
|
||||
const { t, baseUrl, forceBaseUrl, hasUpdatePermission } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Subtitle subtitle={t("base-url-settings.name")} />
|
||||
<Checkbox
|
||||
checked={forceBaseUrl}
|
||||
label={t("base-url-settings.force-base-url")}
|
||||
onChange={this.handleForceBaseUrlChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<InputField
|
||||
label={t("base-url-settings.base-url")}
|
||||
onChange={this.handleBaseUrlChange}
|
||||
value={baseUrl}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleBaseUrlChange = (value: string) => {
|
||||
this.props.onChange(true, value, "baseUrl");
|
||||
};
|
||||
handleForceBaseUrlChange = (value: boolean) => {
|
||||
this.props.onChange(true, value, "forceBaseUrl");
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("config")(BaseUrlSettings);
|
||||
195
scm-ui/src/config/components/form/ConfigForm.js
Normal file
195
scm-ui/src/config/components/form/ConfigForm.js
Normal file
@@ -0,0 +1,195 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { SubmitButton } from "../../../components/buttons/index";
|
||||
import type { Config } from "../../types/Config";
|
||||
import ProxySettings from "./ProxySettings";
|
||||
import GeneralSettings from "./GeneralSettings";
|
||||
import BaseUrlSettings from "./BaseUrlSettings";
|
||||
import AdminSettings from "./AdminSettings";
|
||||
import Notification from "../../../components/Notification";
|
||||
import LoginAttempt from "./LoginAttempt";
|
||||
|
||||
type Props = {
|
||||
submitForm: Config => void,
|
||||
config?: Config,
|
||||
loading?: boolean,
|
||||
t: string => string,
|
||||
configUpdatePermission: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
config: Config,
|
||||
showNotification: boolean,
|
||||
error: {
|
||||
loginAttemptLimitTimeout: boolean,
|
||||
loginAttemptLimit: boolean
|
||||
}
|
||||
};
|
||||
|
||||
class ConfigForm extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
config: {
|
||||
proxyPassword: null,
|
||||
proxyPort: 0,
|
||||
proxyServer: "",
|
||||
proxyUser: null,
|
||||
enableProxy: false,
|
||||
realmDescription: "",
|
||||
enableRepositoryArchive: false,
|
||||
disableGroupingGrid: false,
|
||||
dateFormat: "",
|
||||
anonymousAccessEnabled: false,
|
||||
adminGroups: [],
|
||||
adminUsers: [],
|
||||
baseUrl: "",
|
||||
forceBaseUrl: false,
|
||||
loginAttemptLimit: 0,
|
||||
proxyExcludes: [],
|
||||
skipFailedAuthenticators: false,
|
||||
pluginUrl: "",
|
||||
loginAttemptLimitTimeout: 0,
|
||||
enabledXsrfProtection: true,
|
||||
defaultNamespaceStrategy: "",
|
||||
_links: {}
|
||||
},
|
||||
showNotification: false,
|
||||
error: {
|
||||
loginAttemptLimitTimeout: false,
|
||||
loginAttemptLimit: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { config, configUpdatePermission } = this.props;
|
||||
if (config) {
|
||||
this.setState({ ...this.state, config: { ...config } });
|
||||
}
|
||||
if (!configUpdatePermission) {
|
||||
this.setState({ ...this.state, showNotification: true });
|
||||
}
|
||||
}
|
||||
|
||||
submit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
this.props.submitForm(this.state.config);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { loading, t, configUpdatePermission } = this.props;
|
||||
const config = this.state.config;
|
||||
|
||||
let noPermissionNotification = null;
|
||||
|
||||
if (this.state.showNotification) {
|
||||
noPermissionNotification = (
|
||||
<Notification
|
||||
type={"info"}
|
||||
children={t("config-form.no-permission-notification")}
|
||||
onClose={() => this.onClose()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={this.submit}>
|
||||
{noPermissionNotification}
|
||||
<GeneralSettings
|
||||
realmDescription={config.realmDescription}
|
||||
enableRepositoryArchive={config.enableRepositoryArchive}
|
||||
disableGroupingGrid={config.disableGroupingGrid}
|
||||
dateFormat={config.dateFormat}
|
||||
anonymousAccessEnabled={config.anonymousAccessEnabled}
|
||||
skipFailedAuthenticators={config.skipFailedAuthenticators}
|
||||
pluginUrl={config.pluginUrl}
|
||||
enabledXsrfProtection={config.enabledXsrfProtection}
|
||||
defaultNamespaceStrategy={config.defaultNamespaceStrategy}
|
||||
onChange={(isValid, changedValue, name) =>
|
||||
this.onChange(isValid, changedValue, name)
|
||||
}
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
<LoginAttempt
|
||||
loginAttemptLimit={config.loginAttemptLimit}
|
||||
loginAttemptLimitTimeout={config.loginAttemptLimitTimeout}
|
||||
onChange={(isValid, changedValue, name) =>
|
||||
this.onChange(isValid, changedValue, name)
|
||||
}
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
<BaseUrlSettings
|
||||
baseUrl={config.baseUrl}
|
||||
forceBaseUrl={config.forceBaseUrl}
|
||||
onChange={(isValid, changedValue, name) =>
|
||||
this.onChange(isValid, changedValue, name)
|
||||
}
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
<AdminSettings
|
||||
adminGroups={config.adminGroups}
|
||||
adminUsers={config.adminUsers}
|
||||
onChange={(isValid, changedValue, name) =>
|
||||
this.onChange(isValid, changedValue, name)
|
||||
}
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
<ProxySettings
|
||||
proxyPassword={config.proxyPassword ? config.proxyPassword : ""}
|
||||
proxyPort={config.proxyPort}
|
||||
proxyServer={config.proxyServer ? config.proxyServer : ""}
|
||||
proxyUser={config.proxyUser ? config.proxyUser : ""}
|
||||
enableProxy={config.enableProxy}
|
||||
proxyExcludes={config.proxyExcludes}
|
||||
onChange={(isValid, changedValue, name) =>
|
||||
this.onChange(isValid, changedValue, name)
|
||||
}
|
||||
hasUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
<hr />
|
||||
<SubmitButton
|
||||
loading={loading}
|
||||
label={t("config-form.submit")}
|
||||
disabled={!configUpdatePermission || this.hasError()}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
onChange = (isValid: boolean, changedValue: any, name: string) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
config: {
|
||||
...this.state.config,
|
||||
[name]: changedValue
|
||||
},
|
||||
error: {
|
||||
...this.state.error,
|
||||
[name]: !isValid
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
hasError = () => {
|
||||
return (
|
||||
this.state.error.loginAttemptLimit ||
|
||||
this.state.error.loginAttemptLimitTimeout
|
||||
);
|
||||
};
|
||||
|
||||
onClose = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
showNotification: false
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("config")(ConfigForm);
|
||||
128
scm-ui/src/config/components/form/GeneralSettings.js
Normal file
128
scm-ui/src/config/components/form/GeneralSettings.js
Normal file
@@ -0,0 +1,128 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Checkbox, InputField } from "../../../components/forms/index";
|
||||
|
||||
type Props = {
|
||||
realmDescription: string,
|
||||
enableRepositoryArchive: boolean,
|
||||
disableGroupingGrid: boolean,
|
||||
dateFormat: string,
|
||||
anonymousAccessEnabled: boolean,
|
||||
skipFailedAuthenticators: boolean,
|
||||
pluginUrl: string,
|
||||
enabledXsrfProtection: boolean,
|
||||
defaultNamespaceStrategy: string,
|
||||
t: string => string,
|
||||
onChange: (boolean, any, string) => void,
|
||||
hasUpdatePermission: boolean
|
||||
};
|
||||
|
||||
class GeneralSettings extends React.Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
realmDescription,
|
||||
enableRepositoryArchive,
|
||||
disableGroupingGrid,
|
||||
dateFormat,
|
||||
anonymousAccessEnabled,
|
||||
skipFailedAuthenticators,
|
||||
pluginUrl,
|
||||
enabledXsrfProtection,
|
||||
defaultNamespaceStrategy,
|
||||
hasUpdatePermission
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InputField
|
||||
label={t("general-settings.realm-description")}
|
||||
onChange={this.handleRealmDescriptionChange}
|
||||
value={realmDescription}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<InputField
|
||||
label={t("general-settings.date-format")}
|
||||
onChange={this.handleDateFormatChange}
|
||||
value={dateFormat}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<InputField
|
||||
label={t("general-settings.plugin-url")}
|
||||
onChange={this.handlePluginUrlChange}
|
||||
value={pluginUrl}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<InputField
|
||||
label={t("general-settings.default-namespace-strategy")}
|
||||
onChange={this.handleDefaultNamespaceStrategyChange}
|
||||
value={defaultNamespaceStrategy}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={enabledXsrfProtection}
|
||||
label={t("general-settings.enabled-xsrf-protection")}
|
||||
onChange={this.handleEnabledXsrfProtectionChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={enableRepositoryArchive}
|
||||
label={t("general-settings.enable-repository-archive")}
|
||||
onChange={this.handleEnableRepositoryArchiveChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={disableGroupingGrid}
|
||||
label={t("general-settings.disable-grouping-grid")}
|
||||
onChange={this.handleDisableGroupingGridChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={anonymousAccessEnabled}
|
||||
label={t("general-settings.anonymous-access-enabled")}
|
||||
onChange={this.handleAnonymousAccessEnabledChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<Checkbox
|
||||
checked={skipFailedAuthenticators}
|
||||
label={t("general-settings.skip-failed-authenticators")}
|
||||
onChange={this.handleSkipFailedAuthenticatorsChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleRealmDescriptionChange = (value: string) => {
|
||||
this.props.onChange(true, value, "realmDescription");
|
||||
};
|
||||
handleEnableRepositoryArchiveChange = (value: boolean) => {
|
||||
this.props.onChange(true, value, "enableRepositoryArchive");
|
||||
};
|
||||
handleDisableGroupingGridChange = (value: boolean) => {
|
||||
this.props.onChange(true, value, "disableGroupingGrid");
|
||||
};
|
||||
handleDateFormatChange = (value: string) => {
|
||||
this.props.onChange(true, value, "dateFormat");
|
||||
};
|
||||
handleAnonymousAccessEnabledChange = (value: string) => {
|
||||
this.props.onChange(true, value, "anonymousAccessEnabled");
|
||||
};
|
||||
|
||||
handleSkipFailedAuthenticatorsChange = (value: string) => {
|
||||
this.props.onChange(true, value, "skipFailedAuthenticators");
|
||||
};
|
||||
handlePluginUrlChange = (value: string) => {
|
||||
this.props.onChange(true, value, "pluginUrl");
|
||||
};
|
||||
|
||||
handleEnabledXsrfProtectionChange = (value: boolean) => {
|
||||
this.props.onChange(true, value, "enabledXsrfProtection");
|
||||
};
|
||||
handleDefaultNamespaceStrategyChange = (value: string) => {
|
||||
this.props.onChange(true, value, "defaultNamespaceStrategy");
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("config")(GeneralSettings);
|
||||
87
scm-ui/src/config/components/form/LoginAttempt.js
Normal file
87
scm-ui/src/config/components/form/LoginAttempt.js
Normal file
@@ -0,0 +1,87 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { InputField } from "../../../components/forms/index";
|
||||
import Subtitle from "../../../components/layout/Subtitle";
|
||||
import * as validator from "../../../components/validation";
|
||||
|
||||
type Props = {
|
||||
loginAttemptLimit: number,
|
||||
loginAttemptLimitTimeout: number,
|
||||
t: string => string,
|
||||
onChange: (boolean, any, string) => void,
|
||||
hasUpdatePermission: boolean
|
||||
};
|
||||
|
||||
type State = {
|
||||
loginAttemptLimitError: boolean,
|
||||
loginAttemptLimitTimeoutError: boolean
|
||||
};
|
||||
|
||||
class LoginAttempt extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loginAttemptLimitError: false,
|
||||
loginAttemptLimitTimeoutError: false
|
||||
};
|
||||
}
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
loginAttemptLimit,
|
||||
loginAttemptLimitTimeout,
|
||||
hasUpdatePermission
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Subtitle subtitle={t("login-attempt.name")} />
|
||||
<InputField
|
||||
label={t("login-attempt.login-attempt-limit")}
|
||||
onChange={this.handleLoginAttemptLimitChange}
|
||||
value={loginAttemptLimit}
|
||||
disabled={!hasUpdatePermission}
|
||||
validationError={this.state.loginAttemptLimitError}
|
||||
errorMessage={t("validation.login-attempt-limit-invalid")}
|
||||
/>
|
||||
<InputField
|
||||
label={t("login-attempt.login-attempt-limit-timeout")}
|
||||
onChange={this.handleLoginAttemptLimitTimeoutChange}
|
||||
value={loginAttemptLimitTimeout}
|
||||
disabled={!hasUpdatePermission}
|
||||
validationError={this.state.loginAttemptLimitTimeoutError}
|
||||
errorMessage={t("validation.login-attempt-limit-timeout-invalid")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: set Error in ConfigForm to disable Submit Button!
|
||||
handleLoginAttemptLimitChange = (value: string) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loginAttemptLimitError: !validator.isNumberValid(value)
|
||||
});
|
||||
this.props.onChange(
|
||||
validator.isNumberValid(value),
|
||||
value,
|
||||
"loginAttemptLimit"
|
||||
);
|
||||
};
|
||||
|
||||
handleLoginAttemptLimitTimeoutChange = (value: string) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loginAttemptLimitTimeoutError: !validator.isNumberValid(value)
|
||||
});
|
||||
this.props.onChange(
|
||||
validator.isNumberValid(value),
|
||||
value,
|
||||
"loginAttemptLimitTimeout"
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("config")(LoginAttempt);
|
||||
118
scm-ui/src/config/components/form/ProxySettings.js
Normal file
118
scm-ui/src/config/components/form/ProxySettings.js
Normal file
@@ -0,0 +1,118 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Checkbox, InputField } from "../../../components/forms/index";
|
||||
import Subtitle from "../../../components/layout/Subtitle";
|
||||
import ProxyExcludesTable from "../table/ProxyExcludesTable";
|
||||
import AddEntryToTableField from "../../../components/forms/AddEntryToTableField";
|
||||
|
||||
type Props = {
|
||||
proxyPassword: string,
|
||||
proxyPort: number,
|
||||
proxyServer: string,
|
||||
proxyUser: string,
|
||||
enableProxy: boolean,
|
||||
proxyExcludes: string[],
|
||||
t: string => string,
|
||||
onChange: (boolean, any, string) => void,
|
||||
hasUpdatePermission: boolean
|
||||
};
|
||||
|
||||
class ProxySettings extends React.Component<Props> {
|
||||
render() {
|
||||
const {
|
||||
t,
|
||||
proxyPassword,
|
||||
proxyPort,
|
||||
proxyServer,
|
||||
proxyUser,
|
||||
enableProxy,
|
||||
proxyExcludes,
|
||||
hasUpdatePermission
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Subtitle subtitle={t("proxy-settings.name")} />
|
||||
<Checkbox
|
||||
checked={enableProxy}
|
||||
label={t("proxy-settings.enable-proxy")}
|
||||
onChange={this.handleEnableProxyChange}
|
||||
disabled={!hasUpdatePermission}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-password")}
|
||||
onChange={this.handleProxyPasswordChange}
|
||||
value={proxyPassword}
|
||||
type="password"
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-port")}
|
||||
value={proxyPort}
|
||||
onChange={this.handleProxyPortChange}
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-server")}
|
||||
value={proxyServer}
|
||||
onChange={this.handleProxyServerChange}
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
/>
|
||||
<InputField
|
||||
label={t("proxy-settings.proxy-user")}
|
||||
value={proxyUser}
|
||||
onChange={this.handleProxyUserChange}
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
/>
|
||||
<ProxyExcludesTable
|
||||
proxyExcludes={proxyExcludes}
|
||||
onChange={(isValid, changedValue, name) =>
|
||||
this.props.onChange(isValid, changedValue, name)
|
||||
}
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
/>
|
||||
<AddEntryToTableField
|
||||
addEntry={this.addProxyExclude}
|
||||
disabled={!enableProxy || !hasUpdatePermission}
|
||||
buttonLabel={t("proxy-settings.add-proxy-exclude-button")}
|
||||
fieldLabel={t("proxy-settings.add-proxy-exclude-textfield")}
|
||||
errorMessage={t("proxy-settings.add-proxy-exclude-error")}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleProxyPasswordChange = (value: string) => {
|
||||
this.props.onChange(true, value, "proxyPassword");
|
||||
};
|
||||
handleProxyPortChange = (value: string) => {
|
||||
this.props.onChange(true, value, "proxyPort");
|
||||
};
|
||||
handleProxyServerChange = (value: string) => {
|
||||
this.props.onChange(true, value, "proxyServer");
|
||||
};
|
||||
handleProxyUserChange = (value: string) => {
|
||||
this.props.onChange(true, value, "proxyUser");
|
||||
};
|
||||
handleEnableProxyChange = (value: string) => {
|
||||
this.props.onChange(true, value, "enableProxy");
|
||||
};
|
||||
|
||||
addProxyExclude = (proxyExcludeName: string) => {
|
||||
if (this.isProxyExcludeMember(proxyExcludeName)) {
|
||||
return;
|
||||
}
|
||||
this.props.onChange(
|
||||
true,
|
||||
[...this.props.proxyExcludes, proxyExcludeName],
|
||||
"proxyExcludes"
|
||||
);
|
||||
};
|
||||
|
||||
isProxyExcludeMember = (proxyExcludeName: string) => {
|
||||
return this.props.proxyExcludes.includes(proxyExcludeName);
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("config")(ProxySettings);
|
||||
36
scm-ui/src/config/components/table/AdminGroupTable.js
Normal file
36
scm-ui/src/config/components/table/AdminGroupTable.js
Normal file
@@ -0,0 +1,36 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import ArrayConfigTable from "./ArrayConfigTable";
|
||||
|
||||
type Props = {
|
||||
adminGroups: string[],
|
||||
onChange: (boolean, any, string) => void,
|
||||
disabled: boolean,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
type State = {};
|
||||
|
||||
class AdminGroupTable extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { t, disabled, adminGroups } = this.props;
|
||||
return (
|
||||
<ArrayConfigTable
|
||||
items={adminGroups}
|
||||
label={t("admin-settings.admin-groups")}
|
||||
removeLabel={t("admin-settings.remove-group-button")}
|
||||
onRemove={this.removeEntry}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
removeEntry = (newGroups: string[]) => {
|
||||
this.props.onChange(true, newGroups, "adminGroups");
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("config")(AdminGroupTable);
|
||||
34
scm-ui/src/config/components/table/AdminUserTable.js
Normal file
34
scm-ui/src/config/components/table/AdminUserTable.js
Normal file
@@ -0,0 +1,34 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import ArrayConfigTable from "./ArrayConfigTable";
|
||||
|
||||
type Props = {
|
||||
adminUsers: string[],
|
||||
onChange: (boolean, any, string) => void,
|
||||
disabled: boolean,
|
||||
|
||||
// context props
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class AdminUserTable extends React.Component<Props> {
|
||||
render() {
|
||||
const { adminUsers, t, disabled } = this.props;
|
||||
return (
|
||||
<ArrayConfigTable
|
||||
items={adminUsers}
|
||||
label={t("admin-settings.admin-users")}
|
||||
removeLabel={t("admin-settings.remove-user-button")}
|
||||
onRemove={this.removeEntry}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
removeEntry = (newUsers: string[]) => {
|
||||
this.props.onChange(true, newUsers, "adminUsers");
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("config")(AdminUserTable);
|
||||
48
scm-ui/src/config/components/table/ArrayConfigTable.js
Normal file
48
scm-ui/src/config/components/table/ArrayConfigTable.js
Normal file
@@ -0,0 +1,48 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { RemoveEntryOfTableButton } from "../../../components/buttons";
|
||||
|
||||
type Props = {
|
||||
items: string[],
|
||||
label: string,
|
||||
removeLabel: string,
|
||||
onRemove: (string[], string) => void,
|
||||
disabled: boolean
|
||||
};
|
||||
|
||||
class ArrayConfigTable extends React.Component<Props> {
|
||||
render() {
|
||||
const { label, disabled, removeLabel, items } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<label className="label">{label}</label>
|
||||
<table className="table is-hoverable is-fullwidth">
|
||||
<tbody>
|
||||
{items.map(item => {
|
||||
return (
|
||||
<tr key={item}>
|
||||
<td>{item}</td>
|
||||
<td>
|
||||
<RemoveEntryOfTableButton
|
||||
entryname={item}
|
||||
removeEntry={this.removeEntry}
|
||||
disabled={disabled}
|
||||
label={removeLabel}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
removeEntry = (item: string) => {
|
||||
const newItems = this.props.items.filter(name => name !== item);
|
||||
this.props.onRemove(newItems, item);
|
||||
};
|
||||
}
|
||||
|
||||
export default ArrayConfigTable;
|
||||
34
scm-ui/src/config/components/table/ProxyExcludesTable.js
Normal file
34
scm-ui/src/config/components/table/ProxyExcludesTable.js
Normal file
@@ -0,0 +1,34 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import ArrayConfigTable from "./ArrayConfigTable";
|
||||
|
||||
type Props = {
|
||||
proxyExcludes: string[],
|
||||
t: string => string,
|
||||
onChange: (boolean, any, string) => void,
|
||||
disabled: boolean
|
||||
};
|
||||
|
||||
type State = {};
|
||||
|
||||
class ProxyExcludesTable extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { proxyExcludes, disabled, t } = this.props;
|
||||
return (
|
||||
<ArrayConfigTable
|
||||
items={proxyExcludes}
|
||||
label={t("proxy-settings.proxy-excludes")}
|
||||
removeLabel={t("proxy-settings.remove-proxy-exclude-button")}
|
||||
onRemove={this.removeEntry}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
removeEntry = (newExcludes: string[]) => {
|
||||
this.props.onChange(true, newExcludes, "proxyExcludes");
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("config")(ProxyExcludesTable);
|
||||
56
scm-ui/src/config/containers/Config.js
Normal file
56
scm-ui/src/config/containers/Config.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import { Route } from "react-router";
|
||||
|
||||
import { Page } from "../../components/layout";
|
||||
import { Navigation, NavLink, Section } from "../../components/navigation";
|
||||
import GlobalConfig from "./GlobalConfig";
|
||||
import type { History } from "history";
|
||||
|
||||
type Props = {
|
||||
// context objects
|
||||
t: string => string,
|
||||
match: any,
|
||||
history: History
|
||||
};
|
||||
|
||||
class Config extends React.Component<Props> {
|
||||
stripEndingSlash = (url: string) => {
|
||||
if (url.endsWith("/")) {
|
||||
return url.substring(0, url.length - 2);
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
matchedUrl = () => {
|
||||
return this.stripEndingSlash(this.props.match.url);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
const url = this.matchedUrl();
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="columns">
|
||||
<div className="column is-three-quarters">
|
||||
<Route path={url} exact component={GlobalConfig} />
|
||||
</div>
|
||||
<div className="column">
|
||||
<Navigation>
|
||||
<Section label={t("config.navigation-title")}>
|
||||
<NavLink
|
||||
to={`${url}`}
|
||||
label={t("global-config.navigation-label")}
|
||||
/>
|
||||
</Section>
|
||||
</Navigation>
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("config")(Config);
|
||||
109
scm-ui/src/config/containers/GlobalConfig.js
Normal file
109
scm-ui/src/config/containers/GlobalConfig.js
Normal file
@@ -0,0 +1,109 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import Title from "../../components/layout/Title";
|
||||
import {
|
||||
fetchConfig,
|
||||
getFetchConfigFailure,
|
||||
isFetchConfigPending,
|
||||
getConfig,
|
||||
modifyConfig,
|
||||
isModifyConfigPending,
|
||||
getConfigUpdatePermission,
|
||||
getModifyConfigFailure,
|
||||
modifyConfigReset
|
||||
} from "../modules/config";
|
||||
import { connect } from "react-redux";
|
||||
import ErrorPage from "../../components/ErrorPage";
|
||||
import type { Config } from "../types/Config";
|
||||
import ConfigForm from "../components/form/ConfigForm";
|
||||
import Loading from "../../components/Loading";
|
||||
|
||||
type Props = {
|
||||
loading: boolean,
|
||||
error: Error,
|
||||
config: Config,
|
||||
configUpdatePermission: boolean,
|
||||
|
||||
// dispatch functions
|
||||
modifyConfig: (config: Config, callback?: () => void) => void,
|
||||
fetchConfig: void => void,
|
||||
configReset: void => void,
|
||||
|
||||
// context objects
|
||||
t: string => string
|
||||
};
|
||||
|
||||
class GlobalConfig extends React.Component<Props> {
|
||||
componentDidMount() {
|
||||
this.props.configReset();
|
||||
this.props.fetchConfig();
|
||||
}
|
||||
|
||||
modifyConfig = (config: Config) => {
|
||||
this.props.modifyConfig(config);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { t, error, loading, config, configUpdatePermission } = this.props;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage
|
||||
title={t("global-config.error-title")}
|
||||
subtitle={t("global-config.error-subtitle")}
|
||||
error={error}
|
||||
configUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (loading) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title title={t("global-config.title")} />
|
||||
<ConfigForm
|
||||
submitForm={config => this.modifyConfig(config)}
|
||||
config={config}
|
||||
loading={loading}
|
||||
configUpdatePermission={configUpdatePermission}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
fetchConfig: () => {
|
||||
dispatch(fetchConfig());
|
||||
},
|
||||
modifyConfig: (config: Config, callback?: () => void) => {
|
||||
dispatch(modifyConfig(config, callback));
|
||||
},
|
||||
configReset: () => {
|
||||
dispatch(modifyConfigReset());
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const loading = isFetchConfigPending(state) || isModifyConfigPending(state);
|
||||
const error = getFetchConfigFailure(state) || getModifyConfigFailure(state);
|
||||
const config = getConfig(state);
|
||||
const configUpdatePermission = getConfigUpdatePermission(state);
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
config,
|
||||
configUpdatePermission
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(translate("config")(GlobalConfig));
|
||||
178
scm-ui/src/config/modules/config.js
Normal file
178
scm-ui/src/config/modules/config.js
Normal file
@@ -0,0 +1,178 @@
|
||||
// @flow
|
||||
import { apiClient } from "../../apiclient";
|
||||
import * as types from "../../modules/types";
|
||||
import type { Action } from "../../types/Action";
|
||||
import { isPending } from "../../modules/pending";
|
||||
import { getFailure } from "../../modules/failure";
|
||||
import { Dispatch } from "redux";
|
||||
import type { Config } from "../types/Config";
|
||||
|
||||
export const FETCH_CONFIG = "scm/config/FETCH_CONFIG";
|
||||
export const FETCH_CONFIG_PENDING = `${FETCH_CONFIG}_${types.PENDING_SUFFIX}`;
|
||||
export const FETCH_CONFIG_SUCCESS = `${FETCH_CONFIG}_${types.SUCCESS_SUFFIX}`;
|
||||
export const FETCH_CONFIG_FAILURE = `${FETCH_CONFIG}_${types.FAILURE_SUFFIX}`;
|
||||
|
||||
export const MODIFY_CONFIG = "scm/config/MODIFY_CONFIG";
|
||||
export const MODIFY_CONFIG_PENDING = `${MODIFY_CONFIG}_${types.PENDING_SUFFIX}`;
|
||||
export const MODIFY_CONFIG_SUCCESS = `${MODIFY_CONFIG}_${types.SUCCESS_SUFFIX}`;
|
||||
export const MODIFY_CONFIG_FAILURE = `${MODIFY_CONFIG}_${types.FAILURE_SUFFIX}`;
|
||||
export const MODIFY_CONFIG_RESET = `${MODIFY_CONFIG}_${types.RESET_SUFFIX}`;
|
||||
|
||||
const CONFIG_URL = "config";
|
||||
const CONTENT_TYPE_CONFIG = "application/vnd.scmm-config+json;v=2";
|
||||
|
||||
//fetch config
|
||||
export function fetchConfig() {
|
||||
return function(dispatch: any) {
|
||||
dispatch(fetchConfigPending());
|
||||
return apiClient
|
||||
.get(CONFIG_URL)
|
||||
.then(response => {
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
dispatch(fetchConfigSuccess(data));
|
||||
})
|
||||
.catch(cause => {
|
||||
const error = new Error(`could not fetch config: ${cause.message}`);
|
||||
dispatch(fetchConfigFailure(error));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchConfigPending(): Action {
|
||||
return {
|
||||
type: FETCH_CONFIG_PENDING
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchConfigSuccess(config: Config): Action {
|
||||
return {
|
||||
type: FETCH_CONFIG_SUCCESS,
|
||||
payload: config
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchConfigFailure(error: Error): Action {
|
||||
return {
|
||||
type: FETCH_CONFIG_FAILURE,
|
||||
payload: {
|
||||
error
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// modify config
|
||||
export function modifyConfig(config: Config, callback?: () => void) {
|
||||
return function(dispatch: Dispatch) {
|
||||
dispatch(modifyConfigPending(config));
|
||||
return apiClient
|
||||
.put(config._links.update.href, config, CONTENT_TYPE_CONFIG)
|
||||
.then(() => {
|
||||
dispatch(modifyConfigSuccess(config));
|
||||
if (callback) {
|
||||
callback();
|
||||
}
|
||||
})
|
||||
.catch(cause => {
|
||||
dispatch(
|
||||
modifyConfigFailure(
|
||||
config,
|
||||
new Error(`could not modify config: ${cause.message}`)
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function modifyConfigPending(config: Config): Action {
|
||||
return {
|
||||
type: MODIFY_CONFIG_PENDING,
|
||||
payload: config
|
||||
};
|
||||
}
|
||||
|
||||
export function modifyConfigSuccess(config: Config): Action {
|
||||
return {
|
||||
type: MODIFY_CONFIG_SUCCESS,
|
||||
payload: config
|
||||
};
|
||||
}
|
||||
|
||||
export function modifyConfigFailure(config: Config, error: Error): Action {
|
||||
return {
|
||||
type: MODIFY_CONFIG_FAILURE,
|
||||
payload: {
|
||||
error,
|
||||
config
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function modifyConfigReset() {
|
||||
return {
|
||||
type: MODIFY_CONFIG_RESET
|
||||
};
|
||||
}
|
||||
|
||||
//reducer
|
||||
|
||||
function removeNullValues(config: Config) {
|
||||
if (!config.adminGroups) {
|
||||
config.adminGroups = [];
|
||||
}
|
||||
if (!config.adminUsers) {
|
||||
config.adminUsers = [];
|
||||
}
|
||||
if (!config.proxyExcludes) {
|
||||
config.proxyExcludes = [];
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
function reducer(state: any = {}, action: any = {}) {
|
||||
switch (action.type) {
|
||||
case MODIFY_CONFIG_SUCCESS:
|
||||
case FETCH_CONFIG_SUCCESS:
|
||||
const config = removeNullValues(action.payload);
|
||||
return {
|
||||
...state,
|
||||
entries: config,
|
||||
configUpdatePermission: action.payload._links.update ? true : false
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default reducer;
|
||||
|
||||
// selectors
|
||||
|
||||
export function isFetchConfigPending(state: Object) {
|
||||
return isPending(state, FETCH_CONFIG);
|
||||
}
|
||||
|
||||
export function getFetchConfigFailure(state: Object) {
|
||||
return getFailure(state, FETCH_CONFIG);
|
||||
}
|
||||
|
||||
export function isModifyConfigPending(state: Object) {
|
||||
return isPending(state, MODIFY_CONFIG);
|
||||
}
|
||||
|
||||
export function getModifyConfigFailure(state: Object) {
|
||||
return getFailure(state, MODIFY_CONFIG);
|
||||
}
|
||||
|
||||
export function getConfig(state: Object) {
|
||||
if (state.config && state.config.entries) {
|
||||
return state.config.entries;
|
||||
}
|
||||
}
|
||||
|
||||
export function getConfigUpdatePermission(state: Object) {
|
||||
if (state.config && state.config.configUpdatePermission) {
|
||||
return state.config.configUpdatePermission;
|
||||
}
|
||||
}
|
||||
287
scm-ui/src/config/modules/config.test.js
Normal file
287
scm-ui/src/config/modules/config.test.js
Normal file
@@ -0,0 +1,287 @@
|
||||
//@flow
|
||||
import configureMockStore from "redux-mock-store";
|
||||
import thunk from "redux-thunk";
|
||||
import fetchMock from "fetch-mock";
|
||||
|
||||
import reducer, {
|
||||
FETCH_CONFIG,
|
||||
FETCH_CONFIG_PENDING,
|
||||
FETCH_CONFIG_SUCCESS,
|
||||
FETCH_CONFIG_FAILURE,
|
||||
MODIFY_CONFIG,
|
||||
MODIFY_CONFIG_PENDING,
|
||||
MODIFY_CONFIG_SUCCESS,
|
||||
MODIFY_CONFIG_FAILURE,
|
||||
fetchConfig,
|
||||
fetchConfigSuccess,
|
||||
getFetchConfigFailure,
|
||||
isFetchConfigPending,
|
||||
modifyConfig,
|
||||
isModifyConfigPending,
|
||||
getModifyConfigFailure,
|
||||
getConfig,
|
||||
getConfigUpdatePermission
|
||||
} from "./config";
|
||||
|
||||
const CONFIG_URL = "/scm/api/rest/v2/config";
|
||||
|
||||
const error = new Error("You have an error!");
|
||||
|
||||
const config = {
|
||||
proxyPassword: null,
|
||||
proxyPort: 8080,
|
||||
proxyServer: "proxy.mydomain.com",
|
||||
proxyUser: null,
|
||||
enableProxy: false,
|
||||
realmDescription: "SONIA :: SCM Manager",
|
||||
enableRepositoryArchive: false,
|
||||
disableGroupingGrid: false,
|
||||
dateFormat: "YYYY-MM-DD HH:mm:ss",
|
||||
anonymousAccessEnabled: false,
|
||||
adminGroups: [],
|
||||
adminUsers: [],
|
||||
baseUrl: "http://localhost:8081/scm",
|
||||
forceBaseUrl: false,
|
||||
loginAttemptLimit: -1,
|
||||
proxyExcludes: [],
|
||||
skipFailedAuthenticators: false,
|
||||
pluginUrl:
|
||||
"http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false",
|
||||
loginAttemptLimitTimeout: 300,
|
||||
enabledXsrfProtection: true,
|
||||
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
|
||||
_links: {
|
||||
self: { href: "http://localhost:8081/scm/api/rest/v2/config" },
|
||||
update: { href: "http://localhost:8081/scm/api/rest/v2/config" }
|
||||
}
|
||||
};
|
||||
|
||||
const configWithNullValues = {
|
||||
proxyPassword: null,
|
||||
proxyPort: 8080,
|
||||
proxyServer: "proxy.mydomain.com",
|
||||
proxyUser: null,
|
||||
enableProxy: false,
|
||||
realmDescription: "SONIA :: SCM Manager",
|
||||
enableRepositoryArchive: false,
|
||||
disableGroupingGrid: false,
|
||||
dateFormat: "YYYY-MM-DD HH:mm:ss",
|
||||
anonymousAccessEnabled: false,
|
||||
adminGroups: null,
|
||||
adminUsers: null,
|
||||
baseUrl: "http://localhost:8081/scm",
|
||||
forceBaseUrl: false,
|
||||
loginAttemptLimit: -1,
|
||||
proxyExcludes: null,
|
||||
skipFailedAuthenticators: false,
|
||||
pluginUrl:
|
||||
"http://plugins.scm-manager.org/scm-plugin-backend/api/{version}/plugins?os={os}&arch={arch}&snapshot=false",
|
||||
loginAttemptLimitTimeout: 300,
|
||||
enabledXsrfProtection: true,
|
||||
defaultNamespaceStrategy: "sonia.scm.repository.DefaultNamespaceStrategy",
|
||||
_links: {
|
||||
self: { href: "http://localhost:8081/scm/api/rest/v2/config" },
|
||||
update: { href: "http://localhost:8081/scm/api/rest/v2/config" }
|
||||
}
|
||||
};
|
||||
|
||||
const responseBody = {
|
||||
entries: config,
|
||||
configUpdatePermission: false
|
||||
};
|
||||
|
||||
const response = {
|
||||
headers: { "content-type": "application/json" },
|
||||
responseBody
|
||||
};
|
||||
|
||||
describe("config fetch()", () => {
|
||||
const mockStore = configureMockStore([thunk]);
|
||||
afterEach(() => {
|
||||
fetchMock.reset();
|
||||
fetchMock.restore();
|
||||
});
|
||||
|
||||
it("should successfully fetch config", () => {
|
||||
fetchMock.getOnce(CONFIG_URL, response);
|
||||
|
||||
const expectedActions = [
|
||||
{ type: FETCH_CONFIG_PENDING },
|
||||
{
|
||||
type: FETCH_CONFIG_SUCCESS,
|
||||
payload: response
|
||||
}
|
||||
];
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(fetchConfig()).then(() => {
|
||||
expect(store.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail getting config on HTTP 500", () => {
|
||||
fetchMock.getOnce(CONFIG_URL, {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
return store.dispatch(fetchConfig()).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(FETCH_CONFIG_PENDING);
|
||||
expect(actions[1].type).toEqual(FETCH_CONFIG_FAILURE);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should successfully modify config", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/config", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(modifyConfig(config)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(MODIFY_CONFIG_PENDING);
|
||||
expect(actions[1].type).toEqual(MODIFY_CONFIG_SUCCESS);
|
||||
expect(actions[1].payload).toEqual(config);
|
||||
});
|
||||
});
|
||||
|
||||
it("should call the callback after modifying config", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/config", {
|
||||
status: 204
|
||||
});
|
||||
|
||||
let called = false;
|
||||
const callback = () => {
|
||||
called = true;
|
||||
};
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(modifyConfig(config, callback)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(MODIFY_CONFIG_PENDING);
|
||||
expect(actions[1].type).toEqual(MODIFY_CONFIG_SUCCESS);
|
||||
expect(called).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail modifying config on HTTP 500", () => {
|
||||
fetchMock.putOnce("http://localhost:8081/scm/api/rest/v2/config", {
|
||||
status: 500
|
||||
});
|
||||
|
||||
const store = mockStore({});
|
||||
|
||||
return store.dispatch(modifyConfig(config)).then(() => {
|
||||
const actions = store.getActions();
|
||||
expect(actions[0].type).toEqual(MODIFY_CONFIG_PENDING);
|
||||
expect(actions[1].type).toEqual(MODIFY_CONFIG_FAILURE);
|
||||
expect(actions[1].payload).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("config reducer", () => {
|
||||
it("should update state correctly according to FETCH_CONFIG_SUCCESS action", () => {
|
||||
const newState = reducer({}, fetchConfigSuccess(config));
|
||||
|
||||
expect(newState).toEqual({
|
||||
entries: config,
|
||||
configUpdatePermission: true
|
||||
});
|
||||
});
|
||||
|
||||
it("should set configUpdatePermission to true if update link is present", () => {
|
||||
const newState = reducer({}, fetchConfigSuccess(config));
|
||||
|
||||
expect(newState.configUpdatePermission).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should update state according to FETCH_CONFIG_SUCCESS action", () => {
|
||||
const newState = reducer({}, fetchConfigSuccess(config));
|
||||
expect(newState.entries).toBe(config);
|
||||
});
|
||||
|
||||
it("should return empty arrays for null values", () => {
|
||||
const config = reducer({}, fetchConfigSuccess(configWithNullValues))
|
||||
.entries;
|
||||
expect(config.adminUsers).toEqual([]);
|
||||
expect(config.adminGroups).toEqual([]);
|
||||
expect(config.proxyExcludes).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selector tests", () => {
|
||||
it("should return true, when fetch config is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[FETCH_CONFIG]: true
|
||||
}
|
||||
};
|
||||
expect(isFetchConfigPending(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false, when fetch config is not pending", () => {
|
||||
expect(isFetchConfigPending({})).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error when fetch config did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[FETCH_CONFIG]: error
|
||||
}
|
||||
};
|
||||
expect(getFetchConfigFailure(state)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when fetch config did not fail", () => {
|
||||
expect(getFetchConfigFailure({})).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should return true, when modify group is pending", () => {
|
||||
const state = {
|
||||
pending: {
|
||||
[MODIFY_CONFIG]: true
|
||||
}
|
||||
};
|
||||
expect(isModifyConfigPending(state)).toEqual(true);
|
||||
});
|
||||
|
||||
it("should return false, when modify config is not pending", () => {
|
||||
expect(isModifyConfigPending({})).toEqual(false);
|
||||
});
|
||||
|
||||
it("should return error when modify config did fail", () => {
|
||||
const state = {
|
||||
failure: {
|
||||
[MODIFY_CONFIG]: error
|
||||
}
|
||||
};
|
||||
expect(getModifyConfigFailure(state)).toEqual(error);
|
||||
});
|
||||
|
||||
it("should return undefined when modify config did not fail", () => {
|
||||
expect(getModifyConfigFailure({})).toBe(undefined);
|
||||
});
|
||||
|
||||
it("should return config", () => {
|
||||
const state = {
|
||||
config: {
|
||||
entries: config
|
||||
}
|
||||
};
|
||||
expect(getConfig(state)).toEqual(config);
|
||||
});
|
||||
|
||||
it("should return configUpdatePermission", () => {
|
||||
const state = {
|
||||
config: {
|
||||
configUpdatePermission: true
|
||||
}
|
||||
};
|
||||
expect(getConfigUpdatePermission(state)).toEqual(true);
|
||||
});
|
||||
});
|
||||
27
scm-ui/src/config/types/Config.js
Normal file
27
scm-ui/src/config/types/Config.js
Normal file
@@ -0,0 +1,27 @@
|
||||
//@flow
|
||||
import type { Links } from "../../types/hal";
|
||||
|
||||
export type Config = {
|
||||
proxyPassword: string | null,
|
||||
proxyPort: number,
|
||||
proxyServer: string,
|
||||
proxyUser: string | null,
|
||||
enableProxy: boolean,
|
||||
realmDescription: string,
|
||||
enableRepositoryArchive: boolean,
|
||||
disableGroupingGrid: boolean,
|
||||
dateFormat: string,
|
||||
anonymousAccessEnabled: boolean,
|
||||
adminGroups: string[],
|
||||
adminUsers: string[],
|
||||
baseUrl: string,
|
||||
forceBaseUrl: boolean,
|
||||
loginAttemptLimit: number,
|
||||
proxyExcludes: string[],
|
||||
skipFailedAuthenticators: boolean,
|
||||
pluginUrl: string,
|
||||
loginAttemptLimitTimeout: number,
|
||||
enabledXsrfProtection: boolean,
|
||||
defaultNamespaceStrategy: string,
|
||||
_links: Links
|
||||
};
|
||||
@@ -19,6 +19,8 @@ import Groups from "../groups/containers/Groups";
|
||||
import SingleGroup from "../groups/containers/SingleGroup";
|
||||
import AddGroup from "../groups/containers/AddGroup";
|
||||
|
||||
import Config from "../config/containers/Config";
|
||||
|
||||
type Props = {
|
||||
authenticated?: boolean
|
||||
};
|
||||
@@ -99,6 +101,12 @@ class Main extends React.Component<Props> {
|
||||
component={Groups}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
<ProtectedRoute
|
||||
exact
|
||||
path="/config"
|
||||
component={Config}
|
||||
authenticated={authenticated}
|
||||
/>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import groups from "./groups/modules/groups";
|
||||
import auth from "./modules/auth";
|
||||
import pending from "./modules/pending";
|
||||
import failure from "./modules/failure";
|
||||
import config from "./config/modules/config";
|
||||
|
||||
import type { BrowserHistory } from "history/createBrowserHistory";
|
||||
|
||||
@@ -26,7 +27,8 @@ function createReduxStore(history: BrowserHistory) {
|
||||
repos,
|
||||
repositoryTypes,
|
||||
groups,
|
||||
auth
|
||||
auth,
|
||||
config
|
||||
});
|
||||
|
||||
return createStore(
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
|
||||
import { translate } from "react-i18next";
|
||||
import { AddButton } from "../../components/buttons";
|
||||
import InputField from "../../components/forms/InputField";
|
||||
import { isMemberNameValid } from "./groupValidation";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
addMember: string => void
|
||||
};
|
||||
|
||||
type State = {
|
||||
memberToAdd: string,
|
||||
validationError: boolean
|
||||
};
|
||||
|
||||
class AddMemberField extends React.Component<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
memberToAdd: "",
|
||||
validationError: false
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<div className="field">
|
||||
<InputField
|
||||
label={t("add-member-textfield.label")}
|
||||
errorMessage={t("add-member-textfield.error")}
|
||||
onChange={this.handleAddMemberChange}
|
||||
validationError={this.state.validationError}
|
||||
value={this.state.memberToAdd}
|
||||
onReturnPressed={this.appendMember}
|
||||
/>
|
||||
<AddButton
|
||||
label={t("add-member-button.label")}
|
||||
action={this.addButtonClicked}
|
||||
disabled={!isMemberNameValid(this.state.memberToAdd)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
addButtonClicked = (event: Event) => {
|
||||
event.preventDefault();
|
||||
this.appendMember();
|
||||
};
|
||||
|
||||
appendMember = () => {
|
||||
const { memberToAdd } = this.state;
|
||||
if (isMemberNameValid(memberToAdd)) {
|
||||
this.props.addMember(memberToAdd);
|
||||
this.setState({ ...this.state, memberToAdd: "" });
|
||||
}
|
||||
};
|
||||
|
||||
handleAddMemberChange = (membername: string) => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
memberToAdd: membername,
|
||||
validationError: membername.length > 0 && !isMemberNameValid(membername)
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default translate("groups")(AddMemberField);
|
||||
@@ -6,9 +6,9 @@ import { SubmitButton } from "../../components/buttons";
|
||||
import { translate } from "react-i18next";
|
||||
import type { Group } from "../types/Group";
|
||||
import * as validator from "./groupValidation";
|
||||
import AddMemberField from "./AddMemberField";
|
||||
import MemberNameTable from "./MemberNameTable";
|
||||
import Textarea from "../../components/forms/Textarea";
|
||||
import AddEntryToTableField from "../../components/forms/AddEntryToTableField";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
@@ -96,7 +96,13 @@ class GroupForm extends React.Component<Props, State> {
|
||||
members={this.state.group.members}
|
||||
memberListChanged={this.memberListChanged}
|
||||
/>
|
||||
<AddMemberField addMember={this.addMember} />
|
||||
<AddEntryToTableField
|
||||
addEntry={this.addMember}
|
||||
disabled={false}
|
||||
buttonLabel={t("add-member-button.label")}
|
||||
fieldLabel={t("add-member-textfield.label")}
|
||||
errorMessage={t("add-member-textfield.error")}
|
||||
/>
|
||||
<SubmitButton
|
||||
disabled={!this.isValid()}
|
||||
label={t("group-form.submit")}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { translate } from "react-i18next";
|
||||
import RemoveMemberButton from "./buttons/RemoveMemberButton";
|
||||
import { RemoveEntryOfTableButton } from "../../components/buttons";
|
||||
|
||||
type Props = {
|
||||
members: string[],
|
||||
@@ -24,9 +24,11 @@ class MemberNameTable extends React.Component<Props, State> {
|
||||
<tr key={member}>
|
||||
<td key={member}>{member}</td>
|
||||
<td>
|
||||
<RemoveMemberButton
|
||||
membername={member}
|
||||
removeMember={this.removeMember}
|
||||
<RemoveEntryOfTableButton
|
||||
entryname={member}
|
||||
removeEntry={this.removeEntry}
|
||||
disabled={false}
|
||||
label={t("remove-member-button.label")}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -38,7 +40,7 @@ class MemberNameTable extends React.Component<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
removeMember = (membername: string) => {
|
||||
removeEntry = (membername: string) => {
|
||||
const newMembers = this.props.members.filter(name => name !== membername);
|
||||
this.props.memberListChanged(newMembers);
|
||||
};
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
//@flow
|
||||
import React from "react";
|
||||
import { DeleteButton } from "../../../components/buttons";
|
||||
import { translate } from "react-i18next";
|
||||
import classNames from "classnames";
|
||||
|
||||
type Props = {
|
||||
t: string => string,
|
||||
membername: string,
|
||||
removeMember: string => void
|
||||
};
|
||||
|
||||
type State = {};
|
||||
|
||||
|
||||
|
||||
class RemoveMemberButton extends React.Component<Props, State> {
|
||||
render() {
|
||||
const { t , membername, removeMember} = this.props;
|
||||
return (
|
||||
<div className={classNames("is-pulled-right")}>
|
||||
<DeleteButton
|
||||
label={t("remove-member-button.label")}
|
||||
action={(event: Event) => {
|
||||
event.preventDefault();
|
||||
removeMember(membername);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("groups")(RemoveMemberButton);
|
||||
Reference in New Issue
Block a user