merge with 2.0.0-m3 branch

This commit is contained in:
Sebastian Sdorra
2018-08-30 11:28:26 +02:00
72 changed files with 3308 additions and 437 deletions

View 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;

View File

@@ -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";

View 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;

View File

@@ -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>

View File

@@ -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}

View File

@@ -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) {

View 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;

View 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;

View File

@@ -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")}

View File

@@ -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);
};

View File

@@ -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);
}
});
});

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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);

View 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;

View 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);

View 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);

View 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));

View 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;
}
}

View 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);
});
});

View 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
};

View File

@@ -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>
);

View File

@@ -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(

View File

@@ -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);

View File

@@ -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")}

View File

@@ -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);
};

View File

@@ -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);