From 65ddd751cb39be60a169f8b687782ebe678207f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Thu, 9 Aug 2018 08:15:10 +0200 Subject: [PATCH 01/53] created branch From b03e058c6a004718c3016c737303e01a2665a713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Thu, 9 Aug 2018 09:38:55 +0200 Subject: [PATCH 02/53] add navigation for config --- scm-ui/public/locales/en/commons.json | 3 ++- scm-ui/src/components/navigation/PrimaryNavigation.js | 4 ++++ scm-ui/src/config/containers/Config.js | 9 +++++++++ scm-ui/src/containers/Main.js | 8 ++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 scm-ui/src/config/containers/Config.js diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index 1feaa80a9b..b1bad56993 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -32,7 +32,8 @@ "repositories": "Repositories", "users": "Users", "logout": "Logout", - "groups": "Groups" + "groups": "Groups", + "config": "Configuration" }, "paginator": { "next": "Next", diff --git a/scm-ui/src/components/navigation/PrimaryNavigation.js b/scm-ui/src/components/navigation/PrimaryNavigation.js index 530570b70a..dc81c4a4fc 100644 --- a/scm-ui/src/components/navigation/PrimaryNavigation.js +++ b/scm-ui/src/components/navigation/PrimaryNavigation.js @@ -28,6 +28,10 @@ class PrimaryNavigation extends React.Component { match="/(group|groups)" label={t("primary-navigation.groups")} /> + Here, Config will be shown; + } +} + +export default Config; diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 5272c6117b..cfc2e26f93 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -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 { component={Groups} authenticated={authenticated} /> + ); From 4edf5d6f35d42994919a17fb24299d4980f86672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Thu, 9 Aug 2018 10:18:12 +0200 Subject: [PATCH 03/53] added global navigation --- scm-ui/src/config/containers/Config.js | 52 ++++++++++++++++++-- scm-ui/src/config/containers/GlobalConfig.js | 19 +++++++ 2 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 scm-ui/src/config/containers/GlobalConfig.js diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 47bb8b55b0..0744ae089f 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -1,9 +1,53 @@ -import React, { Component } from "react"; +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 { + stripEndingSlash = (url: string) => { + if (url.endsWith("/")) { + return url.substring(0, url.length - 2); + } + return url; + }; + + matchedUrl = () => { + return this.stripEndingSlash(this.props.match.url); + }; -class Config extends Component { render() { - return
Here, Config will be shown
; + const { t } = this.props; + + const url = this.matchedUrl(); + + return ( + +
+
+ } /> +
+
+ +
+ +
+
+
+
+
+ ); } } -export default Config; +export default translate("config")(Config); diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js new file mode 100644 index 0000000000..fde650413f --- /dev/null +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -0,0 +1,19 @@ +import React from "react"; +import { Page } from "../../components/layout"; +import type { History } from "history"; +import { translate } from "react-i18next"; + +type Props = { + // context objects + t: string => string +}; + +class GlobalConfig extends React.Component { + render() { + const { t } = this.props; + + return
Here, global config will be shown
; + } +} + +export default translate("config")(GlobalConfig); From 52eaafa549b161543e96d066ba3ec56177d7b811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Thu, 9 Aug 2018 11:33:08 +0200 Subject: [PATCH 04/53] refactoring: change title and subtitle to independent components --- scm-ui/src/components/layout/Page.js | 18 ++++++------------ scm-ui/src/components/layout/Subtitle.js | 17 +++++++++++++++++ scm-ui/src/components/layout/Title.js | 17 +++++++++++++++++ 3 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 scm-ui/src/components/layout/Subtitle.js create mode 100644 scm-ui/src/components/layout/Title.js diff --git a/scm-ui/src/components/layout/Page.js b/scm-ui/src/components/layout/Page.js index 77d206d348..f0f2d5d971 100644 --- a/scm-ui/src/components/layout/Page.js +++ b/scm-ui/src/components/layout/Page.js @@ -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 { render() { - const { title, error } = this.props; + const { title, error, subtitle } = this.props; return (
-

{title}

- {this.renderSubtitle()} + + <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) { diff --git a/scm-ui/src/components/layout/Subtitle.js b/scm-ui/src/components/layout/Subtitle.js new file mode 100644 index 0000000000..e55dbde5e4 --- /dev/null +++ b/scm-ui/src/components/layout/Subtitle.js @@ -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; diff --git a/scm-ui/src/components/layout/Title.js b/scm-ui/src/components/layout/Title.js new file mode 100644 index 0000000000..fbb58da14c --- /dev/null +++ b/scm-ui/src/components/layout/Title.js @@ -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; From 9395b77ca2a3073ff8ecb901228423a1b3976ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 11:34:23 +0200 Subject: [PATCH 05/53] set title in global config and not in general config component --- scm-ui/src/config/containers/Config.js | 2 +- scm-ui/src/config/containers/GlobalConfig.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 0744ae089f..a5266a6680 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -32,7 +32,7 @@ class Config extends React.Component<Props> { const url = this.matchedUrl(); return ( - <Page title={t("config.title")}> + <Page> <div className="columns"> <div className="column is-three-quarters"> <Route path={url} exact component={() => <GlobalConfig />} /> diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index fde650413f..e0877e3a09 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -1,7 +1,6 @@ import React from "react"; -import { Page } from "../../components/layout"; -import type { History } from "history"; import { translate } from "react-i18next"; +import Title from "../../components/layout/Title"; type Props = { // context objects @@ -12,7 +11,7 @@ class GlobalConfig extends React.Component<Props> { render() { const { t } = this.props; - return <div>Here, global config will be shown</div>; + return <Title title={t("config.title")} />; } } From 08771624352c5c4f2c9db280246aed0b647bd49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 11:36:10 +0200 Subject: [PATCH 06/53] add endpoint for reading global config --- scm-ui/src/config/modules/config.js | 71 ++++++++++++++ scm-ui/src/config/modules/config.test.js | 116 +++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 scm-ui/src/config/modules/config.js create mode 100644 scm-ui/src/config/modules/config.test.js diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js new file mode 100644 index 0000000000..b951d33ed3 --- /dev/null +++ b/scm-ui/src/config/modules/config.js @@ -0,0 +1,71 @@ +// @flow +import { apiClient } from "../../apiclient"; +import * as types from "../../modules/types"; +import type { Action } from "../../types/Action"; + +export const FETCH_CONFIG = "scm/groups/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}`; + +const CONFIG_URL = "config"; + +//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: any): Action { + return { + type: FETCH_CONFIG_SUCCESS, + payload: config + }; +} + +export function fetchConfigFailure(error: Error): Action { + return { + type: FETCH_CONFIG_FAILURE, + payload: { + error + } + }; +} + +//reducer + +function reducer(state: any = {}, action: any = {}) { + switch (action.type) { + case FETCH_CONFIG_SUCCESS: + return { + ...state, + config: { + entries: action.payload, + configUpdatePermission: action.payload._links.update ? true : false + } + }; + default: + return state; + } +} + +export default reducer; diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js new file mode 100644 index 0000000000..f81df3d522 --- /dev/null +++ b/scm-ui/src/config/modules/config.test.js @@ -0,0 +1,116 @@ +//@flow +import configureMockStore from "redux-mock-store"; +import thunk from "redux-thunk"; +import fetchMock from "fetch-mock"; + +import reducer, { + FETCH_CONFIG_PENDING, + FETCH_CONFIG_SUCCESS, + FETCH_CONFIG_FAILURE, + fetchConfig, + fetchConfigSuccess +} 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 responseBody = { + entries: config +}; + +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(); + }); + }); +}); + +describe("config reducer", () => { + it("should update state correctly according to FETCH_CONFIG_SUCCESS action", () => { + const newState = reducer({}, fetchConfigSuccess(config)); + + expect(newState.config).toEqual({ + entries: config, + configUpdatePermission: true + }); + }); + + it("should set configUpdatePermission to true if update link is present", () => { + const newState = reducer({}, fetchConfigSuccess(config)); + + expect(newState.config.configUpdatePermission).toBeTruthy(); + }); + + it("should update state according to FETCH_GROUP_SUCCESS action", () => { + const newState = reducer({}, fetchConfigSuccess(config)); + expect(newState.config.entries).toBe(config); + }); +}); From 4f8101a8dd860b8ae3a0b1497890e49f69ccf29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 11:36:29 +0200 Subject: [PATCH 07/53] add translation file for config --- scm-ui/public/locales/en/config.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 scm-ui/public/locales/en/config.json diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json new file mode 100644 index 0000000000..c6bd8c6db7 --- /dev/null +++ b/scm-ui/public/locales/en/config.json @@ -0,0 +1,7 @@ +{ + "config": { + "title": "Configuration", + "navigation-title": "Navigation", + "globalConfig-label": "Global Configuration" + } +} From 866a70b816804ad2b1b2f85390c47341e5d8671c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 12:09:50 +0200 Subject: [PATCH 08/53] handle error and loading state --- scm-ui/public/locales/en/config.json | 8 ++- scm-ui/src/config/containers/Config.js | 2 +- scm-ui/src/config/containers/GlobalConfig.js | 61 ++++++++++++++++++-- scm-ui/src/config/modules/config.js | 12 ++++ scm-ui/src/config/modules/config.test.js | 33 ++++++++++- 5 files changed, 107 insertions(+), 9 deletions(-) diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index c6bd8c6db7..dd87bdcae2 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -1,7 +1,11 @@ { "config": { + "navigation-title": "Navigation" + }, + "global-config": { "title": "Configuration", - "navigation-title": "Navigation", - "globalConfig-label": "Global Configuration" + "navigation-label": "Global Configuration", + "error-title": "Error", + "error-subtitle": "Unknown Config Error" } } diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index a5266a6680..0a7d112192 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -40,7 +40,7 @@ class Config extends React.Component<Props> { <div className="column"> <Navigation> <Section label={t("config.navigation-title")}> - <NavLink to={`${url}`} label={t("config.globalConfig-label")} /> + <NavLink to={`${url}`} label={t("global-config.navigation-label")} /> </Section> </Navigation> </div> diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index e0877e3a09..a3fcca70d5 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -1,18 +1,69 @@ import React from "react"; import { translate } from "react-i18next"; import Title from "../../components/layout/Title"; +import { + fetchConfig, + getFetchConfigFailure, + isFetchConfigPending +} from "../modules/config"; +import connect from "react-redux/es/connect/connect"; +import ErrorPage from "../../components/ErrorPage"; +import Loading from "../../components/Loading"; type Props = { + loading: boolean, + error: Error, + // context objects - t: string => string + t: string => string, + fetchConfig: void => void }; class GlobalConfig extends React.Component<Props> { - render() { - const { t } = this.props; + componentDidMount() { + this.props.fetchConfig(); + } - return <Title title={t("config.title")} />; + render() { + const { t, error, loading } = this.props; + + if (error) { + return ( + <ErrorPage + title={t("global-config.error-title")} + subtitle={t("global-config.error-subtitle")} + error={error} + /> + ); + } + + if (loading) { + return <Loading />; + } + + return <Title title={t("global-config.title")} />; } } -export default translate("config")(GlobalConfig); +const mapDispatchToProps = dispatch => { + return { + fetchConfig: () => { + dispatch(fetchConfig()); + } + }; +}; + +const mapStateToProps = state => { + const loading = isFetchConfigPending(state); + const error = getFetchConfigFailure(state); + + return { + loading, + error + }; +}; + +export default connect( + mapStateToProps, + mapDispatchToProps +)(translate("config")(GlobalConfig)); diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index b951d33ed3..062be6fc95 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -2,6 +2,8 @@ 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"; export const FETCH_CONFIG = "scm/groups/FETCH_CONFIG"; export const FETCH_CONFIG_PENDING = `${FETCH_CONFIG}_${types.PENDING_SUFFIX}`; @@ -69,3 +71,13 @@ function reducer(state: any = {}, action: any = {}) { } export default reducer; + +// selectors + +export function isFetchConfigPending(state: Object) { + return isPending(state, FETCH_CONFIG); +} + +export function getFetchConfigFailure(state: Object) { + return getFailure(state, FETCH_CONFIG); +} diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index f81df3d522..86b5db7cbf 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -4,11 +4,14 @@ import thunk from "redux-thunk"; import fetchMock from "fetch-mock"; import reducer, { + FETCH_CONFIG, FETCH_CONFIG_PENDING, FETCH_CONFIG_SUCCESS, FETCH_CONFIG_FAILURE, fetchConfig, - fetchConfigSuccess + fetchConfigSuccess, + getFetchConfigFailure, + isFetchConfigPending } from "./config"; const CONFIG_URL = "/scm/api/rest/v2/config"; @@ -114,3 +117,31 @@ describe("config reducer", () => { expect(newState.config.entries).toBe(config); }); }); + +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); + }); +}); From e0063c5119356166fde2e21e242d9df6e21aa938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 13:05:34 +0200 Subject: [PATCH 09/53] add config to state --- scm-ui/src/createReduxStore.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index 55e0d3f514..8411326b53 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -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( From b0c1b64c43ac6e0cced533e4165d97629226bb18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 13:05:59 +0200 Subject: [PATCH 10/53] correct structure of config state part --- scm-ui/src/config/modules/config.js | 6 ++---- scm-ui/src/config/modules/config.test.js | 8 ++++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 062be6fc95..521dd94495 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -60,10 +60,8 @@ function reducer(state: any = {}, action: any = {}) { case FETCH_CONFIG_SUCCESS: return { ...state, - config: { - entries: action.payload, - configUpdatePermission: action.payload._links.update ? true : false - } + entries: action.payload, + configUpdatePermission: action.payload._links.update ? true : false }; default: return state; diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index 86b5db7cbf..837e7d126f 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -100,7 +100,7 @@ describe("config reducer", () => { it("should update state correctly according to FETCH_CONFIG_SUCCESS action", () => { const newState = reducer({}, fetchConfigSuccess(config)); - expect(newState.config).toEqual({ + expect(newState).toEqual({ entries: config, configUpdatePermission: true }); @@ -109,12 +109,12 @@ describe("config reducer", () => { it("should set configUpdatePermission to true if update link is present", () => { const newState = reducer({}, fetchConfigSuccess(config)); - expect(newState.config.configUpdatePermission).toBeTruthy(); + expect(newState.configUpdatePermission).toBeTruthy(); }); - it("should update state according to FETCH_GROUP_SUCCESS action", () => { + it("should update state according to FETCH_CONFIG_SUCCESS action", () => { const newState = reducer({}, fetchConfigSuccess(config)); - expect(newState.config.entries).toBe(config); + expect(newState.entries).toBe(config); }); }); From 00e078e3ab6630fe7ca4c3502904b8d581ca3eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 16:33:28 +0200 Subject: [PATCH 11/53] remove function because otherwise component is mounted three times instead of one --- scm-ui/src/config/containers/Config.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/config/containers/Config.js b/scm-ui/src/config/containers/Config.js index 0a7d112192..e41d3cc653 100644 --- a/scm-ui/src/config/containers/Config.js +++ b/scm-ui/src/config/containers/Config.js @@ -35,12 +35,15 @@ class Config extends React.Component<Props> { <Page> <div className="columns"> <div className="column is-three-quarters"> - <Route path={url} exact component={() => <GlobalConfig />} /> + <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")} /> + <NavLink + to={`${url}`} + label={t("global-config.navigation-label")} + /> </Section> </Navigation> </div> From a4e1c8b023f58a5584efe0e68123c0a8cd420fd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 16:34:08 +0200 Subject: [PATCH 12/53] renaming --- scm-ui/src/config/modules/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 521dd94495..e7a8e87e98 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -5,7 +5,7 @@ import type { Action } from "../../types/Action"; import { isPending } from "../../modules/pending"; import { getFailure } from "../../modules/failure"; -export const FETCH_CONFIG = "scm/groups/FETCH_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}`; From fa1b82f9102f3dcd5fa7520e07296d4f2dc87bd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 9 Aug 2018 16:44:24 +0200 Subject: [PATCH 13/53] added config modify --- scm-ui/src/config/modules/config.js | 54 +++++++++++++++++++++++ scm-ui/src/config/modules/config.test.js | 55 +++++++++++++++++++++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index e7a8e87e98..2e51693719 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -4,13 +4,20 @@ 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"; 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/FETCH_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}`; + const CONFIG_URL = "config"; +const CONTENT_TYPE_CONFIG = "application/vnd.scmm-config+json;v=2"; //fetch config export function fetchConfig() { @@ -53,6 +60,53 @@ export function fetchConfigFailure(error: Error): Action { }; } +// modify config +export function modifyConfig(config: any, 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: any): Action { + return { + type: MODIFY_CONFIG_PENDING, + payload: config + }; +} + +export function modifyConfigSuccess(config: any): Action { + return { + type: MODIFY_CONFIG_SUCCESS, + payload: config + }; +} + +export function modifyConfigFailure(config: any, error: Error): Action { + return { + type: MODIFY_CONFIG_FAILURE, + payload: { + error, + config + } + }; +} + //reducer function reducer(state: any = {}, action: any = {}) { diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index 837e7d126f..b62ed50bf8 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -8,10 +8,14 @@ import reducer, { FETCH_CONFIG_PENDING, FETCH_CONFIG_SUCCESS, FETCH_CONFIG_FAILURE, + MODIFY_CONFIG_PENDING, + MODIFY_CONFIG_SUCCESS, + MODIFY_CONFIG_FAILURE, fetchConfig, fetchConfigSuccess, getFetchConfigFailure, - isFetchConfigPending + isFetchConfigPending, + modifyConfig } from "./config"; const CONFIG_URL = "/scm/api/rest/v2/config"; @@ -94,6 +98,55 @@ describe("config fetch()", () => { 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", () => { From 10822fa4e0b37b95cc9d04f4fdab5976267fbe75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 08:33:44 +0200 Subject: [PATCH 14/53] add selectors for config content --- scm-ui/src/config/modules/config.js | 12 ++++++++++++ scm-ui/src/config/modules/config.test.js | 22 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 2e51693719..df04e53235 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -133,3 +133,15 @@ export function isFetchConfigPending(state: Object) { export function getFetchConfigFailure(state: Object) { return getFailure(state, FETCH_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; + } +} diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index b62ed50bf8..76f8f1a2b2 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -15,7 +15,9 @@ import reducer, { fetchConfigSuccess, getFetchConfigFailure, isFetchConfigPending, - modifyConfig + modifyConfig, + getConfig, + getConfigUpdatePermission } from "./config"; const CONFIG_URL = "/scm/api/rest/v2/config"; @@ -197,4 +199,22 @@ describe("selector tests", () => { it("should return undefined when fetch config did not fail", () => { expect(getFetchConfigFailure({})).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); + }); }); From bb6f076178afbffed3259466e3639bf20b400ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 08:43:13 +0200 Subject: [PATCH 15/53] add selectors for modifyGroupPending and Failure --- scm-ui/src/config/modules/config.js | 8 ++++++ scm-ui/src/config/modules/config.test.js | 31 +++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index df04e53235..0c231ea1ac 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -134,6 +134,14 @@ 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; diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index 76f8f1a2b2..78b19f10c7 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -8,6 +8,7 @@ import reducer, { FETCH_CONFIG_PENDING, FETCH_CONFIG_SUCCESS, FETCH_CONFIG_FAILURE, + MODIFY_CONFIG, MODIFY_CONFIG_PENDING, MODIFY_CONFIG_SUCCESS, MODIFY_CONFIG_FAILURE, @@ -16,6 +17,8 @@ import reducer, { getFetchConfigFailure, isFetchConfigPending, modifyConfig, + isModifyConfigPending, + getModifyConfigFailure, getConfig, getConfigUpdatePermission } from "./config"; @@ -200,6 +203,32 @@ describe("selector tests", () => { 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: { @@ -209,7 +238,7 @@ describe("selector tests", () => { expect(getConfig(state)).toEqual(config); }); - it("should return configUpdatePermission", () => { + it("should return configUpdatePermission", () => { const state = { config: { configUpdatePermission: true From 5767000eb5957e5df242e653b3b9b1ef2bfdc57d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:33:27 +0200 Subject: [PATCH 16/53] added Config type --- scm-ui/src/config/types/Config.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 scm-ui/src/config/types/Config.js diff --git a/scm-ui/src/config/types/Config.js b/scm-ui/src/config/types/Config.js new file mode 100644 index 0000000000..6c64cd1db7 --- /dev/null +++ b/scm-ui/src/config/types/Config.js @@ -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 +}; From 27ae4dac1a192c67ef4f7db609c881245fa110c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:33:56 +0200 Subject: [PATCH 17/53] show settings and let them modify --- .../config/components/form/AdminSettings.js | 29 ++++ .../config/components/form/BaseUrlSettings.js | 43 ++++++ .../src/config/components/form/ConfigForm.js | 126 ++++++++++++++++ .../config/components/form/GeneralSettings.js | 135 ++++++++++++++++++ .../config/components/form/ProxySettings.js | 82 +++++++++++ 5 files changed, 415 insertions(+) create mode 100644 scm-ui/src/config/components/form/AdminSettings.js create mode 100644 scm-ui/src/config/components/form/BaseUrlSettings.js create mode 100644 scm-ui/src/config/components/form/ConfigForm.js create mode 100644 scm-ui/src/config/components/form/GeneralSettings.js create mode 100644 scm-ui/src/config/components/form/ProxySettings.js diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js new file mode 100644 index 0000000000..5079addca1 --- /dev/null +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -0,0 +1,29 @@ +// @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 = { + adminGroups: string[], + adminUsers: string[], + t: string => string, + onChange: (boolean, any, string) => void +}; +//TODO: Einbauen! +class AdminSettings extends React.Component<Props> { + render() { + const { + t, + adminGroups, + adminUsers + } = this.props; + + return ( + null + ); + } + +} + +export default translate("config")(AdminSettings); diff --git a/scm-ui/src/config/components/form/BaseUrlSettings.js b/scm-ui/src/config/components/form/BaseUrlSettings.js new file mode 100644 index 0000000000..cde4680b48 --- /dev/null +++ b/scm-ui/src/config/components/form/BaseUrlSettings.js @@ -0,0 +1,43 @@ +// @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 +}; + +class BaseUrlSettings extends React.Component<Props> { + render() { + const { t, baseUrl, forceBaseUrl } = 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} + /> + <InputField + label={t("base-url-settings.base-url")} + onChange={this.handleBaseUrlChange} + value={baseUrl} + /> + </div> + ); + } + + handleBaseUrlChange = (value: string) => { + this.props.onChange(true, value, "baseUrl"); + }; + handleForceBaseUrlChange = (value: boolean) => { + this.props.onChange(true, value, "forceBaseUrl"); + }; +} + +export default translate("config")(BaseUrlSettings); diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js new file mode 100644 index 0000000000..ac7325e6fc --- /dev/null +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -0,0 +1,126 @@ +// @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"; + +type Props = { + submitForm: Config => void, + config?: Config, + loading?: boolean, + t: string => string +}; + +type State = { + config: Config +}; + +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: {} + } + }; + } + + componentDidMount() { + const { config } = this.props; + console.log(config); + if (config) { + this.setState({ config: { ...config } }); + } + } + + submit = (event: Event) => { + event.preventDefault(); + this.props.submitForm(this.state.config); + }; + + render() { + const { loading, t } = this.props; + let config = this.state.config; + + return ( + <form onSubmit={this.submit}> + <GeneralSettings + realmDescription={config.realmDescription} + enableRepositoryArchive={config.enableRepositoryArchive} + disableGroupingGrid={config.disableGroupingGrid} + dateFormat={config.dateFormat} + anonymousAccessEnabled={config.anonymousAccessEnabled} + loginAttemptLimit={config.loginAttemptLimit} + skipFailedAuthenticators={config.skipFailedAuthenticators} + pluginUrl={config.pluginUrl} + loginAttemptLimitTimeout={config.loginAttemptLimitTimeout} + enabledXsrfProtection={config.enabledXsrfProtection} + defaultNamespaceStrategy={config.defaultNamespaceStrategy} + onChange={(isValid, changedValue, name) => + this.onChange(isValid, changedValue, name) + } + /> + <BaseUrlSettings + baseUrl={config.baseUrl} + forceBaseUrl={config.forceBaseUrl} + onChange={(isValid, changedValue, name) => + this.onChange(isValid, changedValue, name) + } + /> + <ProxySettings + proxyPassword={config.proxyPassword ? config.proxyPassword : ""} + proxyPort={config.proxyPort} + proxyServer={config.proxyServer ? config.proxyServer : ""} + proxyUser={config.proxyUser ? config.proxyUser : ""} + enableProxy={config.enableProxy} + onChange={(isValid, changedValue, name) => + this.onChange(isValid, changedValue, name) + } + /> + <SubmitButton + // disabled={!this.isValid()} + loading={loading} + label={t("config-form.submit")} + /> + </form> + ); + } + + onChange = (isValid: boolean, changedValue: any, name: string) => { + if (isValid) { + this.setState({ + config: { + ...this.state.config, + [name]: changedValue + } + }); + } + }; +} + +export default translate("config")(ConfigForm); diff --git a/scm-ui/src/config/components/form/GeneralSettings.js b/scm-ui/src/config/components/form/GeneralSettings.js new file mode 100644 index 0000000000..29db1e5288 --- /dev/null +++ b/scm-ui/src/config/components/form/GeneralSettings.js @@ -0,0 +1,135 @@ +// @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, + loginAttemptLimit: number, + skipFailedAuthenticators: boolean, + pluginUrl: string, + loginAttemptLimitTimeout: number, + enabledXsrfProtection: boolean, + defaultNamespaceStrategy: string, + t: string => string, + onChange: (boolean, any, string) => void +}; + +class GeneralSettings extends React.Component<Props> { + render() { + const { + t, + realmDescription, + enableRepositoryArchive, + disableGroupingGrid, + dateFormat, + anonymousAccessEnabled, + loginAttemptLimit, + skipFailedAuthenticators, + pluginUrl, + loginAttemptLimitTimeout, + enabledXsrfProtection, + defaultNamespaceStrategy + } = this.props; + + return ( + <div> + <InputField + label={t("general-settings.realm-description")} + onChange={this.handleRealmDescriptionChange} + value={realmDescription} + /> + <Checkbox + checked={enableRepositoryArchive} + label={t("general-settings.enable-repository-archive")} + onChange={this.handleEnableRepositoryArchiveChange} + /> + <Checkbox + checked={disableGroupingGrid} + label={t("general-settings.disable-grouping-grid")} + onChange={this.handleDisableGroupingGridChange} + /> + <InputField + label={t("general-settings.date-format")} + onChange={this.handleDateFormatChange} + value={dateFormat} + /> + <Checkbox + checked={anonymousAccessEnabled} + label={t("general-settings.anonymous-access-enabled")} + onChange={this.handleAnonymousAccessEnabledChange} + /> + <InputField + label={t("general-settings.login-attempt-limit")} + onChange={this.handleLoginAttemptLimitChange} + value={loginAttemptLimit} + /> + <InputField + label={t("general-settings.login-attempt-limit-timeout")} + onChange={this.handleLoginAttemptLimitTimeoutChange} + value={loginAttemptLimitTimeout} + /> + <Checkbox + checked={skipFailedAuthenticators} + label={t("general-settings.skip-failed-authenticators")} + onChange={this.handleSkipFailedAuthenticatorsChange} + /> + <InputField + label={t("general-settings.plugin-url")} + onChange={this.handlePluginUrlChange} + value={pluginUrl} + /> + <Checkbox + checked={enabledXsrfProtection} + label={t("general-settings.enabled-xsrf-protection")} + onChange={this.handleEnabledXsrfProtectionChange} + /> + <InputField + label={t("general-settings.default-namespace-strategy")} + onChange={this.handleDefaultNamespaceStrategyChange} + value={defaultNamespaceStrategy} + /> + </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"); + }; + handleLoginAttemptLimitChange = (value: string) => { + this.props.onChange(true, value, "loginAttemptLimit"); + }; + handleSkipFailedAuthenticatorsChange = (value: string) => { + this.props.onChange(true, value, "skipFailedAuthenticators"); + }; + handlePluginUrlChange = (value: string) => { + this.props.onChange(true, value, "pluginUrl"); + }; + handleLoginAttemptLimitTimeoutChange = (value: string) => { + this.props.onChange(true, value, "loginAttemptLimitTimeout"); + }; + handleEnabledXsrfProtectionChange = (value: boolean) => { + this.props.onChange(true, value, "enabledXsrfProtection"); + }; + handleDefaultNamespaceStrategyChange = (value: string) => { + this.props.onChange(true, value, "defaultNamespaceStrategy"); + }; +} + +export default translate("config")(GeneralSettings); diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js new file mode 100644 index 0000000000..2938c08a71 --- /dev/null +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -0,0 +1,82 @@ +// @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 = { + proxyPassword: string, + proxyPort: number, + proxyServer: string, + proxyUser: string, + enableProxy: boolean, + proxyExcludes: string[], //TODO: einbauen! + t: string => string, + onChange: (boolean, any, string) => void +}; + +class ProxySettings extends React.Component<Props> { + render() { + const { + t, + proxyPassword, + proxyPort, + proxyServer, + proxyUser, + enableProxy + } = this.props; + + return ( + <div> + <Subtitle subtitle={t("proxy-settings.name")} /> + <Checkbox + checked={enableProxy} + label={t("proxy-settings.enable-proxy")} + onChange={this.handleEnableProxyChange} + /> + <InputField + label={t("proxy-settings.proxy-password")} + onChange={this.handleProxyPasswordChange} + value={proxyPassword} + disable={!enableProxy} + /> + <InputField + label={t("proxy-settings.proxy-port")} + value={proxyPort} + onChange={this.handleProxyPortChange} + disable={!enableProxy} + /> + <InputField + label={t("proxy-settings.proxy-server")} + value={proxyServer} + onChange={this.handleProxyServerChange} + disable={!enableProxy} + /> + <InputField + label={t("proxy-settings.proxy-user")} + value={proxyUser} + onChange={this.handleProxyUserChange} + disable={!enableProxy} + /> + </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"); + }; +} + +export default translate("config")(ProxySettings); From 5acd1ba90a03f99263e2ec80cd6c6703c6494d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:34:12 +0200 Subject: [PATCH 18/53] add translations --- scm-ui/public/locales/en/config.json | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index dd87bdcae2..df2a12a6dc 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -7,5 +7,39 @@ "navigation-label": "Global Configuration", "error-title": "Error", "error-subtitle": "Unknown Config Error" + }, + "config-form": { + "submit": "Submit" + }, + "proxy-settings": { + "name": "Proxy Settings", + "proxy-password": "Proxy Password", + "proxy-port": "Proxy Port", + "proxy-server": "Proxy Server", + "proxy-user": "Proxy User", + "enable-proxy": "Enable Proxy", + "proxy-excludes": "Proxy Excludes" + }, + "base-url-settings": { + "name": "Base URL Settings", + "base-url": "Base URL", + "force-base-url": "Force Base URL" + }, + "admin-settings": { + "admin-groups": "Admin Groups", + "admin-user": "Admin Users" + }, + "general-settings": { + "realm-description": "Realm Description", + "enable-repository-archive": "Enable Repository Archive", + "disable-grouping-grid": "Disable Grouping Grid", + "date-format": "Date Format", + "anonymous-access-enabled": "Anonymous Access Enabled", + "login-attempt-limit": "Login Attempt Limit", + "skip-failed-authenticators": "Skip Failed Authenticators", + "plugin-url": "Plugin URL", + "login-attempt-limit-timeout": "Login Attempt Limit Timeout", + "enabled-xsrf-protection": "Enabled XSRF Protection", + "default-namespace-strategy": "Default Namespace Strategy" } } From 0df136da19660c1d8f971e0da0495f352c3ce2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:34:31 +0200 Subject: [PATCH 19/53] add disabled option to input field --- scm-ui/src/components/forms/InputField.js | 29 ++++++++++++++++------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/scm-ui/src/components/forms/InputField.js b/scm-ui/src/components/forms/InputField.js index be657348e5..9833739461 100644 --- a/scm-ui/src/components/forms/InputField.js +++ b/scm-ui/src/components/forms/InputField.js @@ -11,7 +11,8 @@ type Props = { onChange: string => void, onReturnPressed?: () => void, validationError: boolean, - errorMessage: string + errorMessage: string, + disable?: 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, + disable + } = 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={disable} /> </div> {helper} From 335ac2df248a3ec37a3a4a8e040e49a16644489e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:35:19 +0200 Subject: [PATCH 20/53] use Config type --- scm-ui/src/config/modules/config.js | 13 +++++++------ scm-ui/src/config/modules/config.test.js | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 0c231ea1ac..4534dbaeb6 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -5,13 +5,14 @@ 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/FETCH_CONFIG"; +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}`; @@ -44,7 +45,7 @@ export function fetchConfigPending(): Action { }; } -export function fetchConfigSuccess(config: any): Action { +export function fetchConfigSuccess(config: Config): Action { return { type: FETCH_CONFIG_SUCCESS, payload: config @@ -61,7 +62,7 @@ export function fetchConfigFailure(error: Error): Action { } // modify config -export function modifyConfig(config: any, callback?: () => void) { +export function modifyConfig(config: Config, callback?: () => void) { return function(dispatch: Dispatch) { dispatch(modifyConfigPending(config)); return apiClient @@ -83,21 +84,21 @@ export function modifyConfig(config: any, callback?: () => void) { }; } -export function modifyConfigPending(config: any): Action { +export function modifyConfigPending(config: Config): Action { return { type: MODIFY_CONFIG_PENDING, payload: config }; } -export function modifyConfigSuccess(config: any): Action { +export function modifyConfigSuccess(config: Config): Action { return { type: MODIFY_CONFIG_SUCCESS, payload: config }; } -export function modifyConfigFailure(config: any, error: Error): Action { +export function modifyConfigFailure(config: Config, error: Error): Action { return { type: MODIFY_CONFIG_FAILURE, payload: { diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index 78b19f10c7..d991bd929a 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -57,7 +57,8 @@ const config = { }; const responseBody = { - entries: config + entries: config, + configUpdatePermission: false }; const response = { From f2850370dd9967f1c8f874dc32ffe0c78fd1ee93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Mon, 13 Aug 2018 16:35:43 +0200 Subject: [PATCH 21/53] add modification of config --- scm-ui/src/config/containers/GlobalConfig.js | 47 ++++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index a3fcca70d5..a6da5f41bf 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -4,28 +4,46 @@ import Title from "../../components/layout/Title"; import { fetchConfig, getFetchConfigFailure, - isFetchConfigPending + isFetchConfigPending, + getConfig, + modifyConfig, + isModifyConfigPending } from "../modules/config"; import connect from "react-redux/es/connect/connect"; import ErrorPage from "../../components/ErrorPage"; +import type { Config } from "../types/Config"; +import ConfigForm from "../components/form/ConfigForm"; import Loading from "../../components/Loading"; +import type { User } from "../../users/types/User"; +import type { History } from "history"; type Props = { loading: boolean, error: Error, - + config: Config, + // dispatch functions + modifyConfig: (config: User, callback?: () => void) => void, // context objects t: string => string, - fetchConfig: void => void + fetchConfig: void => void, + history: History }; class GlobalConfig extends React.Component<Props> { + configModified = (config: Config) => () => { + this.props.history.push(`/config`); + }; + componentDidMount() { this.props.fetchConfig(); } + modifyConfig = (config: Config) => { + this.props.modifyConfig(config, this.configModified(config)); + }; + render() { - const { t, error, loading } = this.props; + const { t, error, loading, config } = this.props; if (error) { return ( @@ -36,12 +54,20 @@ class GlobalConfig extends React.Component<Props> { /> ); } - if (loading) { return <Loading />; } - return <Title title={t("global-config.title")} />; + return ( + <div> + <Title title={t("global-config.title")} /> + <ConfigForm + submitForm={config => this.modifyConfig(config)} + config={config} + loading={loading} + /> + </div> + ); } } @@ -49,17 +75,22 @@ const mapDispatchToProps = dispatch => { return { fetchConfig: () => { dispatch(fetchConfig()); + }, + modifyConfig: (config: Config, callback?: () => void) => { + dispatch(modifyConfig(config, callback)); } }; }; const mapStateToProps = state => { - const loading = isFetchConfigPending(state); + const loading = isFetchConfigPending(state) || isModifyConfigPending(state); //TODO: Button lädt so nicht, sondern gesamte Seite const error = getFetchConfigFailure(state); + const config = getConfig(state); return { loading, - error + error, + config }; }; From 41589d785d61a2c143b12e57500d747c3d090dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 10:55:39 +0200 Subject: [PATCH 22/53] added view and edit of admin user and groups --- scm-ui/public/locales/en/config.json | 11 ++- .../buttons/RemoveAdminGroupButton.js | 34 +++++++++ .../buttons/RemoveAdminUserButton.js | 34 +++++++++ .../components/fields/AddAdminGroupField.js | 71 +++++++++++++++++++ .../components/fields/AddAdminUserField.js | 71 +++++++++++++++++++ .../config/components/form/AdminSettings.js | 59 +++++++++++++-- .../src/config/components/form/ConfigForm.js | 15 +++- .../components/table/AdminGroupTable.js | 47 ++++++++++++ .../config/components/table/AdminUserTable.js | 47 ++++++++++++ scm-ui/src/config/containers/GlobalConfig.js | 13 +++- 10 files changed, 390 insertions(+), 12 deletions(-) create mode 100644 scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js create mode 100644 scm-ui/src/config/components/buttons/RemoveAdminUserButton.js create mode 100644 scm-ui/src/config/components/fields/AddAdminGroupField.js create mode 100644 scm-ui/src/config/components/fields/AddAdminUserField.js create mode 100644 scm-ui/src/config/components/table/AdminGroupTable.js create mode 100644 scm-ui/src/config/components/table/AdminUserTable.js diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index df2a12a6dc..53d41e682d 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -26,8 +26,17 @@ "force-base-url": "Force Base URL" }, "admin-settings": { + "name": "Administration Settings", "admin-groups": "Admin Groups", - "admin-user": "Admin Users" + "admin-users": "Admin Users", + "remove-group-button": "Remove Admin Group", + "remove-user-button": "Remove Admin User", + "add-group-error": "The group name you want to add is not valid", + "add-group-textfield": "Add group you want to add to admin groups here", + "add-group-button": "Add Admin Group", + "add-user-error": "The user name you want to add is not valid", + "add-user-textfield": "Add user you want to add to admin users here", + "add-user-button": "Add Admin User" }, "general-settings": { "realm-description": "Realm Description", diff --git a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js new file mode 100644 index 0000000000..c7d8f8d85a --- /dev/null +++ b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js @@ -0,0 +1,34 @@ +//@flow +import React from "react"; +import { DeleteButton } from "../../../components/buttons"; +import { translate } from "react-i18next"; +import classNames from "classnames"; + +type Props = { + t: string => string, + groupname: string, + removeGroup: string => void +}; + +type State = {}; + + + +class RemoveAdminGroupButton extends React.Component<Props, State> { + render() { + const { t , groupname, removeGroup} = this.props; + return ( + <div className={classNames("is-pulled-right")}> + <DeleteButton + label={t("admin-settings.remove-group-button")} + action={(event: Event) => { + event.preventDefault(); + removeGroup(groupname); + }} + /> + </div> + ); + } +} + +export default translate("config")(RemoveAdminGroupButton); diff --git a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js new file mode 100644 index 0000000000..6bdf06cd28 --- /dev/null +++ b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js @@ -0,0 +1,34 @@ +//@flow +import React from "react"; +import { DeleteButton } from "../../../components/buttons"; +import { translate } from "react-i18next"; +import classNames from "classnames"; + +type Props = { + t: string => string, + username: string, + removeUser: string => void +}; + +type State = {}; + + + +class RemoveAdminUserButton extends React.Component<Props, State> { + render() { + const { t , username, removeUser} = this.props; + return ( + <div className={classNames("is-pulled-right")}> + <DeleteButton + label={t("admin-settings.remove-user-button")} + action={(event: Event) => { + event.preventDefault(); + removeUser(username); + }} + /> + </div> + ); + } +} + +export default translate("config")(RemoveAdminUserButton); diff --git a/scm-ui/src/config/components/fields/AddAdminGroupField.js b/scm-ui/src/config/components/fields/AddAdminGroupField.js new file mode 100644 index 0000000000..ed694355d8 --- /dev/null +++ b/scm-ui/src/config/components/fields/AddAdminGroupField.js @@ -0,0 +1,71 @@ +//@flow +import React from "react"; + +import { translate } from "react-i18next"; +import { AddButton } from "../../../components/buttons"; +import InputField from "../../../components/forms/InputField"; + +type Props = { + t: string => string, + addGroup: string => void +}; + +type State = { + groupToAdd: string, + //validationError: boolean +}; + +class AddAdminGroupField extends React.Component<Props, State> { + constructor(props) { + super(props); + this.state = { + groupToAdd: "", + //validationError: false + }; + } + + render() { + const { t } = this.props; + return ( + <div className="field"> + <InputField + + label={t("admin-settings.add-group-textfield")} + errorMessage={t("admin-settings.add-group-error")} + onChange={this.handleAddGroupChange} + validationError={false} + value={this.state.groupToAdd} + onReturnPressed={this.appendGroup} + /> + <AddButton + label={t("admin-settings.add-group-button")} + action={this.addButtonClicked} + //disabled={!isMemberNameValid(this.state.memberToAdd)} + /> + </div> + ); + } + + addButtonClicked = (event: Event) => { + event.preventDefault(); + this.appendGroup(); + }; + + appendGroup = () => { + const { groupToAdd } = this.state; + //if (isMemberNameValid(memberToAdd)) { + this.props.addGroup(groupToAdd); + this.setState({ ...this.state, groupToAdd: "" }); + // } + }; + + handleAddGroupChange = (groupname: string) => { + this.setState({ + ...this.state, + groupToAdd: groupname, + //validationError: membername.length > 0 && !isMemberNameValid(membername) + }); + }; +} + +export default translate("config")(AddAdminGroupField); diff --git a/scm-ui/src/config/components/fields/AddAdminUserField.js b/scm-ui/src/config/components/fields/AddAdminUserField.js new file mode 100644 index 0000000000..116144ff3c --- /dev/null +++ b/scm-ui/src/config/components/fields/AddAdminUserField.js @@ -0,0 +1,71 @@ +//@flow +import React from "react"; + +import { translate } from "react-i18next"; +import { AddButton } from "../../../components/buttons"; +import InputField from "../../../components/forms/InputField"; + +type Props = { + t: string => string, + addUser: string => void +}; + +type State = { + userToAdd: string, + //validationError: boolean +}; + +class AddAdminUserField extends React.Component<Props, State> { + constructor(props) { + super(props); + this.state = { + userToAdd: "", + //validationError: false + }; + } + + render() { + const { t } = this.props; + return ( + <div className="field"> + <InputField + + label={t("admin-settings.add-user-textfield")} + errorMessage={t("admin-settings.add-user-error")} + onChange={this.handleAddUserChange} + validationError={false} + value={this.state.userToAdd} + onReturnPressed={this.appendUser} + /> + <AddButton + label={t("admin-settings.add-user-button")} + action={this.addButtonClicked} + //disabled={!isMemberNameValid(this.state.memberToAdd)} + /> + </div> + ); + } + + addButtonClicked = (event: Event) => { + event.preventDefault(); + this.appendUser(); + }; + + appendUser = () => { + const { userToAdd } = this.state; + //if (isMemberNameValid(memberToAdd)) { + this.props.addUser(userToAdd); + this.setState({ ...this.state, userToAdd: "" }); + // } + }; + + handleAddUserChange = (username: string) => { + this.setState({ + ...this.state, + userToAdd: username, + //validationError: membername.length > 0 && !isMemberNameValid(membername) + }); + }; +} + +export default translate("config")(AddAdminUserField); diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index 5079addca1..bcc6efbbfe 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -1,8 +1,12 @@ // @flow import React from "react"; import { translate } from "react-i18next"; -import { Checkbox, InputField } from "../../../components/forms/index"; import Subtitle from "../../../components/layout/Subtitle"; +import AdminGroupTable from "../table/AdminGroupTable"; +import ProxySettings from "./ProxySettings"; +import AdminUserTable from "../table/AdminUserTable"; +import AddAdminGroupField from "../fields/AddAdminGroupField"; +import AddAdminUserField from "../fields/AddAdminUserField"; type Props = { adminGroups: string[], @@ -13,17 +17,58 @@ type Props = { //TODO: Einbauen! class AdminSettings extends React.Component<Props> { render() { - const { - t, - adminGroups, - adminUsers - } = this.props; + const { t, adminGroups, adminUsers } = this.props; return ( - null + <div> + <Subtitle subtitle={t("admin-settings.name")} /> + <AdminGroupTable + adminGroups={adminGroups} + onChange={(isValid, changedValue, name) => + this.props.onChange(isValid, changedValue, name) + } + /> + <AddAdminGroupField addGroup={this.addGroup} /> + <AdminUserTable + adminUsers={adminUsers} + onChange={(isValid, changedValue, name) => + this.props.onChange(isValid, changedValue, name) + } + /> + <AddAdminUserField addUser={this.addUser} /> + </div> ); } + addGroup = (groupname: string) => { + if (this.isAdminGroupMember(groupname)) { + return; + } + this.props.onChange( + true, + [...this.props.adminGroups, groupname], + "adminGroups" + ); + }; + + isAdminGroupMember = (groupname: string) => { + return this.props.adminGroups.includes(groupname); + }; + + addUser = (username: string) => { + if (this.isAdminUserMember(username)) { + return; + } + this.props.onChange( + true, + [...this.props.adminUsers, username], + "adminUsers" + ); + }; + + isAdminUserMember = (username: string) => { + return this.props.adminUsers.includes(username); + }; } export default translate("config")(AdminSettings); diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index ac7325e6fc..ec75f3f579 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -6,12 +6,14 @@ import type { Config } from "../../types/Config"; import ProxySettings from "./ProxySettings"; import GeneralSettings from "./GeneralSettings"; import BaseUrlSettings from "./BaseUrlSettings"; +import AdminSettings from "./AdminSettings"; type Props = { submitForm: Config => void, config?: Config, loading?: boolean, - t: string => string + t: string => string, + configUpdatePermission: boolean }; type State = { @@ -85,6 +87,7 @@ class ConfigForm extends React.Component<Props, State> { this.onChange(isValid, changedValue, name) } /> + <hr /> <BaseUrlSettings baseUrl={config.baseUrl} forceBaseUrl={config.forceBaseUrl} @@ -92,6 +95,15 @@ class ConfigForm extends React.Component<Props, State> { this.onChange(isValid, changedValue, name) } /> + <hr /> + <AdminSettings + adminGroups={config.adminGroups} + adminUsers={config.adminUsers} + onChange={(isValid, changedValue, name) => + this.onChange(isValid, changedValue, name) + } + /> + <hr /> <ProxySettings proxyPassword={config.proxyPassword ? config.proxyPassword : ""} proxyPort={config.proxyPort} @@ -102,6 +114,7 @@ class ConfigForm extends React.Component<Props, State> { this.onChange(isValid, changedValue, name) } /> + <hr /> <SubmitButton // disabled={!this.isValid()} loading={loading} diff --git a/scm-ui/src/config/components/table/AdminGroupTable.js b/scm-ui/src/config/components/table/AdminGroupTable.js new file mode 100644 index 0000000000..94e54ba747 --- /dev/null +++ b/scm-ui/src/config/components/table/AdminGroupTable.js @@ -0,0 +1,47 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import RemoveAdminGroupButton from "../buttons/RemoveAdminGroupButton"; + +type Props = { + adminGroups: string[], + t: string => string, + onChange: (boolean, any, string) => void +}; + +type State = {}; + +class AdminGroupTable extends React.Component<Props, State> { + render() { + const { t } = this.props; + return ( + <div> + <label className="label">{t("admin-settings.admin-groups")}</label> + <table className="table is-hoverable is-fullwidth"> + <tbody> + {this.props.adminGroups.map(group => { + return ( + <tr key={group}> + <td key={group}>{group}</td> + <td> + <RemoveAdminGroupButton + groupname={group} + removeGroup={this.removeGroup} + /> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); + } + + removeGroup = (groupname: string) => { + const newGroups = this.props.adminGroups.filter(name => name !== groupname); + this.props.onChange(true, newGroups, "adminGroups"); + }; +} + +export default translate("config")(AdminGroupTable); diff --git a/scm-ui/src/config/components/table/AdminUserTable.js b/scm-ui/src/config/components/table/AdminUserTable.js new file mode 100644 index 0000000000..8f1cbe11c4 --- /dev/null +++ b/scm-ui/src/config/components/table/AdminUserTable.js @@ -0,0 +1,47 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import RemoveAdminUserButton from "../buttons/RemoveAdminUserButton"; + +type Props = { + adminUsers: string[], + t: string => string, + onChange: (boolean, any, string) => void +}; + +type State = {}; + +class AdminUserTable extends React.Component<Props, State> { + render() { + const { t } = this.props; + return ( + <div> + <label className="label">{t("admin-settings.admin-users")}</label> + <table className="table is-hoverable is-fullwidth"> + <tbody> + {this.props.adminUsers.map(user => { + return ( + <tr key={user}> + <td key={user}>{user}</td> + <td> + <RemoveAdminUserButton + username={user} + removeUser={this.removeUser} + /> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); + } + + removeUser = (username: string) => { + const newUsers = this.props.adminUsers.filter(name => name !== username); + this.props.onChange(true, newUsers, "adminUsers"); + }; +} + +export default translate("config")(AdminUserTable); diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index a6da5f41bf..56e0a06c9b 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -7,7 +7,8 @@ import { isFetchConfigPending, getConfig, modifyConfig, - isModifyConfigPending + isModifyConfigPending, + getConfigUpdatePermission } from "../modules/config"; import connect from "react-redux/es/connect/connect"; import ErrorPage from "../../components/ErrorPage"; @@ -21,6 +22,7 @@ type Props = { loading: boolean, error: Error, config: Config, + configUpdatePermission: boolean, // dispatch functions modifyConfig: (config: User, callback?: () => void) => void, // context objects @@ -31,6 +33,7 @@ type Props = { class GlobalConfig extends React.Component<Props> { configModified = (config: Config) => () => { + this.props.fetchConfig(); this.props.history.push(`/config`); }; @@ -39,11 +42,12 @@ class GlobalConfig extends React.Component<Props> { } modifyConfig = (config: Config) => { + console.log(config); this.props.modifyConfig(config, this.configModified(config)); }; render() { - const { t, error, loading, config } = this.props; + const { t, error, loading, config, configUpdatePermission } = this.props; if (error) { return ( @@ -51,6 +55,7 @@ class GlobalConfig extends React.Component<Props> { title={t("global-config.error-title")} subtitle={t("global-config.error-subtitle")} error={error} + configUpdatePermission={configUpdatePermission} /> ); } @@ -86,11 +91,13 @@ const mapStateToProps = state => { const loading = isFetchConfigPending(state) || isModifyConfigPending(state); //TODO: Button lädt so nicht, sondern gesamte Seite const error = getFetchConfigFailure(state); const config = getConfig(state); + const configUpdatePermission = getConfigUpdatePermission(state); return { loading, error, - config + config, + configUpdatePermission }; }; From 998595ccd969c76529cab0f59cba218c246753a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 11:18:10 +0200 Subject: [PATCH 23/53] remove to do since it is done --- scm-ui/src/config/components/form/AdminSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index bcc6efbbfe..1486ce56af 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -14,7 +14,7 @@ type Props = { t: string => string, onChange: (boolean, any, string) => void }; -//TODO: Einbauen! + class AdminSettings extends React.Component<Props> { render() { const { t, adminGroups, adminUsers } = this.props; From 0452b95d105d2c4b3362d50ca039582493f095fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 11:18:28 +0200 Subject: [PATCH 24/53] add proxy exclude view and edit --- scm-ui/public/locales/en/config.json | 6 +- .../buttons/RemoveProxyExcludeButton.js | 34 +++++++++ .../components/fields/AddProxyExcludeField.js | 71 +++++++++++++++++++ .../src/config/components/form/ConfigForm.js | 1 + .../config/components/form/ProxySettings.js | 27 ++++++- .../components/table/ProxyExcludesTable.js | 47 ++++++++++++ 6 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js create mode 100644 scm-ui/src/config/components/fields/AddProxyExcludeField.js create mode 100644 scm-ui/src/config/components/table/ProxyExcludesTable.js diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 53d41e682d..5f1db2a549 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -18,7 +18,11 @@ "proxy-server": "Proxy Server", "proxy-user": "Proxy User", "enable-proxy": "Enable Proxy", - "proxy-excludes": "Proxy Excludes" + "proxy-excludes": "Proxy Excludes", + "remove-proxy-exclude-button": "Remove Proxy Exclude", + "add-proxy-exclude-error": "The proxy exclude you want to add is not valid", + "add-proxy-exclude-textfield": "Add proxy exclude you want to add to proxy excludes here", + "add-proxy-exclude-button": "Add Proxy Exclude" }, "base-url-settings": { "name": "Base URL Settings", diff --git a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js new file mode 100644 index 0000000000..254f8c8225 --- /dev/null +++ b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js @@ -0,0 +1,34 @@ +//@flow +import React from "react"; +import { DeleteButton } from "../../../components/buttons"; +import { translate } from "react-i18next"; +import classNames from "classnames"; + +type Props = { + t: string => string, + proxyExcludeName: string, + removeProxyExclude: string => void +}; + +type State = {}; + + + +class RemoveProxyExcludeButton extends React.Component<Props, State> { + render() { + const { t , proxyExcludeName, removeProxyExclude} = this.props; + return ( + <div className={classNames("is-pulled-right")}> + <DeleteButton + label={t("proxy-settings.remove-proxy-exclude-button")} + action={(event: Event) => { + event.preventDefault(); + removeProxyExclude(proxyExcludeName); + }} + /> + </div> + ); + } +} + +export default translate("config")(RemoveProxyExcludeButton); diff --git a/scm-ui/src/config/components/fields/AddProxyExcludeField.js b/scm-ui/src/config/components/fields/AddProxyExcludeField.js new file mode 100644 index 0000000000..a5c8b3370d --- /dev/null +++ b/scm-ui/src/config/components/fields/AddProxyExcludeField.js @@ -0,0 +1,71 @@ +//@flow +import React from "react"; + +import { translate } from "react-i18next"; +import { AddButton } from "../../../components/buttons"; +import InputField from "../../../components/forms/InputField"; + +type Props = { + t: string => string, + addProxyExclude: string => void +}; + +type State = { + proxyExcludeToAdd: string, + //validationError: boolean +}; + +class AddProxyExcludeField extends React.Component<Props, State> { + constructor(props) { + super(props); + this.state = { + proxyExcludeToAdd: "", + //validationError: false + }; + } + + render() { + const { t } = this.props; + return ( + <div className="field"> + <InputField + + label={t("proxy-settings.add-proxy-exclude-textfield")} + errorMessage={t("proxy-settings.add-proxy-exclude-error")} + onChange={this.handleAddProxyExcludeChange} + validationError={false} + value={this.state.proxyExcludeToAdd} + onReturnPressed={this.appendProxyExclude} + /> + <AddButton + label={t("proxy-settings.add-proxy-exclude-button")} + action={this.addButtonClicked} + //disabled={!isMemberNameValid(this.state.memberToAdd)} + /> + </div> + ); + } + + addButtonClicked = (event: Event) => { + event.preventDefault(); + this.appendProxyExclude(); + }; + + appendProxyExclude = () => { + const { proxyExcludeToAdd } = this.state; + //if (isMemberNameValid(memberToAdd)) { + this.props.addProxyExclude(proxyExcludeToAdd); + this.setState({ ...this.state, proxyExcludeToAdd: "" }); + // } + }; + + handleAddProxyExcludeChange = (username: string) => { + this.setState({ + ...this.state, + proxyExcludeToAdd: username, + //validationError: membername.length > 0 && !isMemberNameValid(membername) + }); + }; +} + +export default translate("config")(AddProxyExcludeField); diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index ec75f3f579..4d05a816a4 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -110,6 +110,7 @@ class ConfigForm extends React.Component<Props, State> { 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) } diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index 2938c08a71..9904c739c0 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -3,6 +3,8 @@ 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 AddProxyExcludeField from "../fields/AddProxyExcludeField"; type Props = { proxyPassword: string, @@ -23,7 +25,8 @@ class ProxySettings extends React.Component<Props> { proxyPort, proxyServer, proxyUser, - enableProxy + enableProxy, + proxyExcludes } = this.props; return ( @@ -58,6 +61,13 @@ class ProxySettings extends React.Component<Props> { onChange={this.handleProxyUserChange} disable={!enableProxy} /> + <ProxyExcludesTable + proxyExcludes={proxyExcludes} + onChange={(isValid, changedValue, name) => + this.props.onChange(isValid, changedValue, name) + } + /> + <AddProxyExcludeField addProxyExclude={this.addProxyExclude} /> </div> ); } @@ -77,6 +87,21 @@ class ProxySettings extends React.Component<Props> { 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); diff --git a/scm-ui/src/config/components/table/ProxyExcludesTable.js b/scm-ui/src/config/components/table/ProxyExcludesTable.js new file mode 100644 index 0000000000..04f3126425 --- /dev/null +++ b/scm-ui/src/config/components/table/ProxyExcludesTable.js @@ -0,0 +1,47 @@ +//@flow +import React from "react"; +import { translate } from "react-i18next"; +import RemoveProxyExcludeButton from "../buttons/RemoveProxyExcludeButton"; + +type Props = { + proxyExcludes: string[], + t: string => string, + onChange: (boolean, any, string) => void +}; + +type State = {}; + +class ProxyExcludesTable extends React.Component<Props, State> { + render() { + const { t } = this.props; + return ( + <div> + <label className="label">{t("proxy-settings.proxy-excludes")}</label> + <table className="table is-hoverable is-fullwidth"> + <tbody> + {this.props.proxyExcludes.map(excludes => { + return ( + <tr key={excludes}> + <td key={excludes}>{excludes}</td> + <td> + <RemoveProxyExcludeButton + proxyExcludeName={excludes} + removeProxyExclude={this.removeProxyExclude} + /> + </td> + </tr> + ); + })} + </tbody> + </table> + </div> + ); + } + + removeProxyExclude = (excludename: string) => { + const newExcludes = this.props.proxyExcludes.filter(name => name !== excludename); + this.props.onChange(true, newExcludes, "proxyExcludes"); + }; +} + +export default translate("config")(ProxyExcludesTable); From d2c85606090c4d1d826636316a3705f6e834b313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 11:26:45 +0200 Subject: [PATCH 25/53] remove unsued imports and disable editing if proxy is disabled --- .../buttons/RemoveProxyExcludeButton.js | 6 ++-- .../components/fields/AddProxyExcludeField.js | 7 ++-- .../config/components/form/AdminSettings.js | 1 - .../config/components/form/ProxySettings.js | 3 +- .../components/table/ProxyExcludesTable.js | 34 +++++++++++-------- 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js index 254f8c8225..ba177024fe 100644 --- a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js +++ b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js @@ -1,13 +1,14 @@ //@flow import React from "react"; -import { DeleteButton } from "../../../components/buttons"; +import {DeleteButton} from "../../../components/buttons"; import { translate } from "react-i18next"; import classNames from "classnames"; type Props = { t: string => string, proxyExcludeName: string, - removeProxyExclude: string => void + removeProxyExclude: string => void, + disable: boolean }; type State = {}; @@ -25,6 +26,7 @@ class RemoveProxyExcludeButton extends React.Component<Props, State> { event.preventDefault(); removeProxyExclude(proxyExcludeName); }} + disabled={this.props.disable} /> </div> ); diff --git a/scm-ui/src/config/components/fields/AddProxyExcludeField.js b/scm-ui/src/config/components/fields/AddProxyExcludeField.js index a5c8b3370d..c76d8ae807 100644 --- a/scm-ui/src/config/components/fields/AddProxyExcludeField.js +++ b/scm-ui/src/config/components/fields/AddProxyExcludeField.js @@ -2,12 +2,13 @@ import React from "react"; import { translate } from "react-i18next"; -import { AddButton } from "../../../components/buttons"; +import {AddButton} from "../../../components/buttons"; import InputField from "../../../components/forms/InputField"; type Props = { t: string => string, - addProxyExclude: string => void + addProxyExclude: string => void, + disable: boolean }; type State = { @@ -36,10 +37,12 @@ class AddProxyExcludeField extends React.Component<Props, State> { validationError={false} value={this.state.proxyExcludeToAdd} onReturnPressed={this.appendProxyExclude} + disable={this.props.disable} /> <AddButton label={t("proxy-settings.add-proxy-exclude-button")} action={this.addButtonClicked} + disabled={this.props.disable} //disabled={!isMemberNameValid(this.state.memberToAdd)} /> </div> diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index 1486ce56af..dcde12f91f 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -3,7 +3,6 @@ import React from "react"; import { translate } from "react-i18next"; import Subtitle from "../../../components/layout/Subtitle"; import AdminGroupTable from "../table/AdminGroupTable"; -import ProxySettings from "./ProxySettings"; import AdminUserTable from "../table/AdminUserTable"; import AddAdminGroupField from "../fields/AddAdminGroupField"; import AddAdminUserField from "../fields/AddAdminUserField"; diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index 9904c739c0..0a8666f805 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -66,8 +66,9 @@ class ProxySettings extends React.Component<Props> { onChange={(isValid, changedValue, name) => this.props.onChange(isValid, changedValue, name) } + disable={!enableProxy} /> - <AddProxyExcludeField addProxyExclude={this.addProxyExclude} /> + <AddProxyExcludeField addProxyExclude={this.addProxyExclude} disable={!enableProxy}/> </div> ); } diff --git a/scm-ui/src/config/components/table/ProxyExcludesTable.js b/scm-ui/src/config/components/table/ProxyExcludesTable.js index 04f3126425..b8102c4f63 100644 --- a/scm-ui/src/config/components/table/ProxyExcludesTable.js +++ b/scm-ui/src/config/components/table/ProxyExcludesTable.js @@ -6,7 +6,8 @@ import RemoveProxyExcludeButton from "../buttons/RemoveProxyExcludeButton"; type Props = { proxyExcludes: string[], t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + disable: boolean }; type State = {}; @@ -19,19 +20,20 @@ class ProxyExcludesTable extends React.Component<Props, State> { <label className="label">{t("proxy-settings.proxy-excludes")}</label> <table className="table is-hoverable is-fullwidth"> <tbody> - {this.props.proxyExcludes.map(excludes => { - return ( - <tr key={excludes}> - <td key={excludes}>{excludes}</td> - <td> - <RemoveProxyExcludeButton - proxyExcludeName={excludes} - removeProxyExclude={this.removeProxyExclude} - /> - </td> - </tr> - ); - })} + {this.props.proxyExcludes.map(excludes => { + return ( + <tr key={excludes}> + <td key={excludes}>{excludes}</td> + <td> + <RemoveProxyExcludeButton + proxyExcludeName={excludes} + removeProxyExclude={this.removeProxyExclude} + disable={this.props.disable} + /> + </td> + </tr> + ); + })} </tbody> </table> </div> @@ -39,7 +41,9 @@ class ProxyExcludesTable extends React.Component<Props, State> { } removeProxyExclude = (excludename: string) => { - const newExcludes = this.props.proxyExcludes.filter(name => name !== excludename); + const newExcludes = this.props.proxyExcludes.filter( + name => name !== excludename + ); this.props.onChange(true, newExcludes, "proxyExcludes"); }; } From fbc3b458317239bf66fc8e11c29f82579e1ed39e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 11:37:21 +0200 Subject: [PATCH 26/53] add error handling if modify config failed --- scm-ui/src/config/containers/GlobalConfig.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index 56e0a06c9b..5d18f86226 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -8,7 +8,8 @@ import { getConfig, modifyConfig, isModifyConfigPending, - getConfigUpdatePermission + getConfigUpdatePermission, + getModifyConfigFailure } from "../modules/config"; import connect from "react-redux/es/connect/connect"; import ErrorPage from "../../components/ErrorPage"; @@ -89,7 +90,7 @@ const mapDispatchToProps = dispatch => { const mapStateToProps = state => { const loading = isFetchConfigPending(state) || isModifyConfigPending(state); //TODO: Button lädt so nicht, sondern gesamte Seite - const error = getFetchConfigFailure(state); + const error = getFetchConfigFailure(state) || getModifyConfigFailure(state); const config = getConfig(state); const configUpdatePermission = getConfigUpdatePermission(state); From 08328bb95f1998dced1ec295f26055929f3490f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 12:12:59 +0200 Subject: [PATCH 27/53] disable everything if user has no permission to edit config --- scm-ui/src/components/forms/Checkbox.js | 6 +++-- scm-ui/src/components/forms/InputField.js | 6 ++--- .../buttons/RemoveAdminGroupButton.js | 7 ++++-- .../buttons/RemoveAdminUserButton.js | 9 ++++---- .../buttons/RemoveProxyExcludeButton.js | 4 ++-- .../components/fields/AddAdminGroupField.js | 7 ++++-- .../components/fields/AddAdminUserField.js | 14 +++++++----- .../components/fields/AddProxyExcludeField.js | 15 ++++++------- .../config/components/form/AdminSettings.js | 13 +++++++---- .../config/components/form/BaseUrlSettings.js | 7 ++++-- .../src/config/components/form/ConfigForm.js | 8 +++++-- .../config/components/form/GeneralSettings.js | 17 ++++++++++++-- .../config/components/form/ProxySettings.js | 22 ++++++++++++------- .../components/table/AdminGroupTable.js | 6 +++-- .../config/components/table/AdminUserTable.js | 7 ++++-- .../components/table/ProxyExcludesTable.js | 4 ++-- scm-ui/src/config/containers/GlobalConfig.js | 1 + 17 files changed, 100 insertions(+), 53 deletions(-) diff --git a/scm-ui/src/components/forms/Checkbox.js b/scm-ui/src/components/forms/Checkbox.js index b37f951e2b..72dfbf4af8 100644 --- a/scm-ui/src/components/forms/Checkbox.js +++ b/scm-ui/src/components/forms/Checkbox.js @@ -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> diff --git a/scm-ui/src/components/forms/InputField.js b/scm-ui/src/components/forms/InputField.js index 9833739461..6f87683939 100644 --- a/scm-ui/src/components/forms/InputField.js +++ b/scm-ui/src/components/forms/InputField.js @@ -12,7 +12,7 @@ type Props = { onReturnPressed?: () => void, validationError: boolean, errorMessage: string, - disable?: boolean + disabled?: boolean }; class InputField extends React.Component<Props> { @@ -60,7 +60,7 @@ class InputField extends React.Component<Props> { value, validationError, errorMessage, - disable + disabled } = this.props; const errorView = validationError ? "is-danger" : ""; const helper = validationError ? ( @@ -82,7 +82,7 @@ class InputField extends React.Component<Props> { value={value} onChange={this.handleInput} onKeyPress={this.handleKeyPress} - disabled={disable} + disabled={disabled} /> </div> {helper} diff --git a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js index c7d8f8d85a..c593483409 100644 --- a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js +++ b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js @@ -3,11 +3,13 @@ import React from "react"; import { DeleteButton } from "../../../components/buttons"; import { translate } from "react-i18next"; import classNames from "classnames"; +import {InputField} from "../../../components/forms"; type Props = { t: string => string, groupname: string, - removeGroup: string => void + removeGroup: string => void, + disabled: boolean }; type State = {}; @@ -16,7 +18,7 @@ type State = {}; class RemoveAdminGroupButton extends React.Component<Props, State> { render() { - const { t , groupname, removeGroup} = this.props; + const { t , groupname, removeGroup, disabled} = this.props; return ( <div className={classNames("is-pulled-right")}> <DeleteButton @@ -25,6 +27,7 @@ class RemoveAdminGroupButton extends React.Component<Props, State> { event.preventDefault(); removeGroup(groupname); }} + disabled={disabled} /> </div> ); diff --git a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js index 6bdf06cd28..bd4a02a18a 100644 --- a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js +++ b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js @@ -3,20 +3,20 @@ import React from "react"; import { DeleteButton } from "../../../components/buttons"; import { translate } from "react-i18next"; import classNames from "classnames"; +import { InputField } from "../../../components/forms"; type Props = { t: string => string, username: string, - removeUser: string => void + removeUser: string => void, + disabled: boolean }; type State = {}; - - class RemoveAdminUserButton extends React.Component<Props, State> { render() { - const { t , username, removeUser} = this.props; + const { t, username, removeUser, disabled } = this.props; return ( <div className={classNames("is-pulled-right")}> <DeleteButton @@ -25,6 +25,7 @@ class RemoveAdminUserButton extends React.Component<Props, State> { event.preventDefault(); removeUser(username); }} + disabled={disabled} /> </div> ); diff --git a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js index ba177024fe..acdfd6292a 100644 --- a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js +++ b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js @@ -8,7 +8,7 @@ type Props = { t: string => string, proxyExcludeName: string, removeProxyExclude: string => void, - disable: boolean + disabled: boolean }; type State = {}; @@ -26,7 +26,7 @@ class RemoveProxyExcludeButton extends React.Component<Props, State> { event.preventDefault(); removeProxyExclude(proxyExcludeName); }} - disabled={this.props.disable} + disabled={this.props.disabled} /> </div> ); diff --git a/scm-ui/src/config/components/fields/AddAdminGroupField.js b/scm-ui/src/config/components/fields/AddAdminGroupField.js index ed694355d8..0991056b66 100644 --- a/scm-ui/src/config/components/fields/AddAdminGroupField.js +++ b/scm-ui/src/config/components/fields/AddAdminGroupField.js @@ -7,7 +7,8 @@ import InputField from "../../../components/forms/InputField"; type Props = { t: string => string, - addGroup: string => void + addGroup: string => void, + disabled: boolean }; type State = { @@ -25,7 +26,7 @@ class AddAdminGroupField extends React.Component<Props, State> { } render() { - const { t } = this.props; + const { t, disabled } = this.props; return ( <div className="field"> <InputField @@ -36,10 +37,12 @@ class AddAdminGroupField extends React.Component<Props, State> { validationError={false} value={this.state.groupToAdd} onReturnPressed={this.appendGroup} + disabled={disabled} /> <AddButton label={t("admin-settings.add-group-button")} action={this.addButtonClicked} + disabled={disabled} //disabled={!isMemberNameValid(this.state.memberToAdd)} /> </div> diff --git a/scm-ui/src/config/components/fields/AddAdminUserField.js b/scm-ui/src/config/components/fields/AddAdminUserField.js index 116144ff3c..025ed6cb4d 100644 --- a/scm-ui/src/config/components/fields/AddAdminUserField.js +++ b/scm-ui/src/config/components/fields/AddAdminUserField.js @@ -7,11 +7,12 @@ import InputField from "../../../components/forms/InputField"; type Props = { t: string => string, - addUser: string => void + addUser: string => void, + disabled: boolean }; type State = { - userToAdd: string, + userToAdd: string //validationError: boolean }; @@ -19,27 +20,28 @@ class AddAdminUserField extends React.Component<Props, State> { constructor(props) { super(props); this.state = { - userToAdd: "", + userToAdd: "" //validationError: false }; } render() { - const { t } = this.props; + const { t, disabled } = this.props; return ( <div className="field"> <InputField - label={t("admin-settings.add-user-textfield")} errorMessage={t("admin-settings.add-user-error")} onChange={this.handleAddUserChange} validationError={false} value={this.state.userToAdd} onReturnPressed={this.appendUser} + disabled={disabled} /> <AddButton label={t("admin-settings.add-user-button")} action={this.addButtonClicked} + disabled={disabled} //disabled={!isMemberNameValid(this.state.memberToAdd)} /> </div> @@ -62,7 +64,7 @@ class AddAdminUserField extends React.Component<Props, State> { handleAddUserChange = (username: string) => { this.setState({ ...this.state, - userToAdd: username, + userToAdd: username //validationError: membername.length > 0 && !isMemberNameValid(membername) }); }; diff --git a/scm-ui/src/config/components/fields/AddProxyExcludeField.js b/scm-ui/src/config/components/fields/AddProxyExcludeField.js index c76d8ae807..bb7e005b02 100644 --- a/scm-ui/src/config/components/fields/AddProxyExcludeField.js +++ b/scm-ui/src/config/components/fields/AddProxyExcludeField.js @@ -2,17 +2,17 @@ import React from "react"; import { translate } from "react-i18next"; -import {AddButton} from "../../../components/buttons"; +import { AddButton } from "../../../components/buttons"; import InputField from "../../../components/forms/InputField"; type Props = { t: string => string, addProxyExclude: string => void, - disable: boolean + disabled: boolean }; type State = { - proxyExcludeToAdd: string, + proxyExcludeToAdd: string //validationError: boolean }; @@ -20,7 +20,7 @@ class AddProxyExcludeField extends React.Component<Props, State> { constructor(props) { super(props); this.state = { - proxyExcludeToAdd: "", + proxyExcludeToAdd: "" //validationError: false }; } @@ -30,19 +30,18 @@ class AddProxyExcludeField extends React.Component<Props, State> { return ( <div className="field"> <InputField - label={t("proxy-settings.add-proxy-exclude-textfield")} errorMessage={t("proxy-settings.add-proxy-exclude-error")} onChange={this.handleAddProxyExcludeChange} validationError={false} value={this.state.proxyExcludeToAdd} onReturnPressed={this.appendProxyExclude} - disable={this.props.disable} + disabled={this.props.disabled} /> <AddButton label={t("proxy-settings.add-proxy-exclude-button")} action={this.addButtonClicked} - disabled={this.props.disable} + disabled={this.props.disabled} //disabled={!isMemberNameValid(this.state.memberToAdd)} /> </div> @@ -65,7 +64,7 @@ class AddProxyExcludeField extends React.Component<Props, State> { handleAddProxyExcludeChange = (username: string) => { this.setState({ ...this.state, - proxyExcludeToAdd: username, + proxyExcludeToAdd: username //validationError: membername.length > 0 && !isMemberNameValid(membername) }); }; diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index dcde12f91f..b7a39f234c 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -11,12 +11,13 @@ type Props = { adminGroups: string[], adminUsers: string[], t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + hasUpdatePermission: boolean }; class AdminSettings extends React.Component<Props> { render() { - const { t, adminGroups, adminUsers } = this.props; + const { t, adminGroups, adminUsers, hasUpdatePermission } = this.props; return ( <div> @@ -26,15 +27,19 @@ class AdminSettings extends React.Component<Props> { onChange={(isValid, changedValue, name) => this.props.onChange(isValid, changedValue, name) } + disabled={!hasUpdatePermission} + /> + <AddAdminGroupField addGroup={this.addGroup} disabled={!hasUpdatePermission} /> - <AddAdminGroupField addGroup={this.addGroup} /> <AdminUserTable adminUsers={adminUsers} onChange={(isValid, changedValue, name) => this.props.onChange(isValid, changedValue, name) } + disabled={!hasUpdatePermission} + /> + <AddAdminUserField addUser={this.addUser} disabled={!hasUpdatePermission} /> - <AddAdminUserField addUser={this.addUser} /> </div> ); } diff --git a/scm-ui/src/config/components/form/BaseUrlSettings.js b/scm-ui/src/config/components/form/BaseUrlSettings.js index cde4680b48..e7b80ed924 100644 --- a/scm-ui/src/config/components/form/BaseUrlSettings.js +++ b/scm-ui/src/config/components/form/BaseUrlSettings.js @@ -8,12 +8,13 @@ type Props = { baseUrl: string, forceBaseUrl: boolean, t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + hasUpdatePermission: boolean }; class BaseUrlSettings extends React.Component<Props> { render() { - const { t, baseUrl, forceBaseUrl } = this.props; + const { t, baseUrl, forceBaseUrl, hasUpdatePermission } = this.props; return ( <div> @@ -22,11 +23,13 @@ class BaseUrlSettings extends React.Component<Props> { 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> ); diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index 4d05a816a4..db0a79e7bf 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -66,9 +66,8 @@ class ConfigForm extends React.Component<Props, State> { }; render() { - const { loading, t } = this.props; + const { loading, t, configUpdatePermission } = this.props; let config = this.state.config; - return ( <form onSubmit={this.submit}> <GeneralSettings @@ -86,6 +85,7 @@ class ConfigForm extends React.Component<Props, State> { onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name) } + hasUpdatePermission={configUpdatePermission} /> <hr /> <BaseUrlSettings @@ -94,6 +94,7 @@ class ConfigForm extends React.Component<Props, State> { onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name) } + hasUpdatePermission={configUpdatePermission} /> <hr /> <AdminSettings @@ -102,6 +103,7 @@ class ConfigForm extends React.Component<Props, State> { onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name) } + hasUpdatePermission={configUpdatePermission} /> <hr /> <ProxySettings @@ -114,12 +116,14 @@ class ConfigForm extends React.Component<Props, State> { onChange={(isValid, changedValue, name) => this.onChange(isValid, changedValue, name) } + hasUpdatePermission={configUpdatePermission} /> <hr /> <SubmitButton // disabled={!this.isValid()} loading={loading} label={t("config-form.submit")} + disabled={!configUpdatePermission} /> </form> ); diff --git a/scm-ui/src/config/components/form/GeneralSettings.js b/scm-ui/src/config/components/form/GeneralSettings.js index 29db1e5288..321f247a2a 100644 --- a/scm-ui/src/config/components/form/GeneralSettings.js +++ b/scm-ui/src/config/components/form/GeneralSettings.js @@ -16,7 +16,8 @@ type Props = { enabledXsrfProtection: boolean, defaultNamespaceStrategy: string, t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + hasUpdatePermission: boolean }; class GeneralSettings extends React.Component<Props> { @@ -33,7 +34,8 @@ class GeneralSettings extends React.Component<Props> { pluginUrl, loginAttemptLimitTimeout, enabledXsrfProtection, - defaultNamespaceStrategy + defaultNamespaceStrategy, + hasUpdatePermission } = this.props; return ( @@ -42,56 +44,67 @@ class GeneralSettings extends React.Component<Props> { label={t("general-settings.realm-description")} onChange={this.handleRealmDescriptionChange} value={realmDescription} + 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} /> <InputField label={t("general-settings.date-format")} onChange={this.handleDateFormatChange} value={dateFormat} + disabled={!hasUpdatePermission} /> <Checkbox checked={anonymousAccessEnabled} label={t("general-settings.anonymous-access-enabled")} onChange={this.handleAnonymousAccessEnabledChange} + disabled={!hasUpdatePermission} /> <InputField label={t("general-settings.login-attempt-limit")} onChange={this.handleLoginAttemptLimitChange} value={loginAttemptLimit} + disabled={!hasUpdatePermission} /> <InputField label={t("general-settings.login-attempt-limit-timeout")} onChange={this.handleLoginAttemptLimitTimeoutChange} value={loginAttemptLimitTimeout} + disabled={!hasUpdatePermission} /> <Checkbox checked={skipFailedAuthenticators} label={t("general-settings.skip-failed-authenticators")} onChange={this.handleSkipFailedAuthenticatorsChange} + disabled={!hasUpdatePermission} /> <InputField label={t("general-settings.plugin-url")} onChange={this.handlePluginUrlChange} value={pluginUrl} + disabled={!hasUpdatePermission} /> <Checkbox checked={enabledXsrfProtection} label={t("general-settings.enabled-xsrf-protection")} onChange={this.handleEnabledXsrfProtectionChange} + disabled={!hasUpdatePermission} /> <InputField label={t("general-settings.default-namespace-strategy")} onChange={this.handleDefaultNamespaceStrategyChange} value={defaultNamespaceStrategy} + disabled={!hasUpdatePermission} /> </div> ); diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index 0a8666f805..c21d4e807f 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -14,7 +14,8 @@ type Props = { enableProxy: boolean, proxyExcludes: string[], //TODO: einbauen! t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + hasUpdatePermission: boolean }; class ProxySettings extends React.Component<Props> { @@ -26,7 +27,8 @@ class ProxySettings extends React.Component<Props> { proxyServer, proxyUser, enableProxy, - proxyExcludes + proxyExcludes, + hasUpdatePermission } = this.props; return ( @@ -36,39 +38,43 @@ class ProxySettings extends React.Component<Props> { 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} - disable={!enableProxy} + disabled={!enableProxy || !hasUpdatePermission} /> <InputField label={t("proxy-settings.proxy-port")} value={proxyPort} onChange={this.handleProxyPortChange} - disable={!enableProxy} + disabled={!enableProxy || !hasUpdatePermission} /> <InputField label={t("proxy-settings.proxy-server")} value={proxyServer} onChange={this.handleProxyServerChange} - disable={!enableProxy} + disabled={!enableProxy || !hasUpdatePermission} /> <InputField label={t("proxy-settings.proxy-user")} value={proxyUser} onChange={this.handleProxyUserChange} - disable={!enableProxy} + disabled={!enableProxy || !hasUpdatePermission} /> <ProxyExcludesTable proxyExcludes={proxyExcludes} onChange={(isValid, changedValue, name) => this.props.onChange(isValid, changedValue, name) } - disable={!enableProxy} + disabled={!enableProxy || !hasUpdatePermission} + /> + <AddProxyExcludeField + addProxyExclude={this.addProxyExclude} + disabled={!enableProxy || !hasUpdatePermission} /> - <AddProxyExcludeField addProxyExclude={this.addProxyExclude} disable={!enableProxy}/> </div> ); } diff --git a/scm-ui/src/config/components/table/AdminGroupTable.js b/scm-ui/src/config/components/table/AdminGroupTable.js index 94e54ba747..d5566e7ce1 100644 --- a/scm-ui/src/config/components/table/AdminGroupTable.js +++ b/scm-ui/src/config/components/table/AdminGroupTable.js @@ -6,14 +6,15 @@ import RemoveAdminGroupButton from "../buttons/RemoveAdminGroupButton"; type Props = { adminGroups: string[], t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + disabled: boolean }; type State = {}; class AdminGroupTable extends React.Component<Props, State> { render() { - const { t } = this.props; + const { t, disabled } = this.props; return ( <div> <label className="label">{t("admin-settings.admin-groups")}</label> @@ -27,6 +28,7 @@ class AdminGroupTable extends React.Component<Props, State> { <RemoveAdminGroupButton groupname={group} removeGroup={this.removeGroup} + disabled={disabled} /> </td> </tr> diff --git a/scm-ui/src/config/components/table/AdminUserTable.js b/scm-ui/src/config/components/table/AdminUserTable.js index 8f1cbe11c4..9a840351e0 100644 --- a/scm-ui/src/config/components/table/AdminUserTable.js +++ b/scm-ui/src/config/components/table/AdminUserTable.js @@ -2,18 +2,20 @@ import React from "react"; import { translate } from "react-i18next"; import RemoveAdminUserButton from "../buttons/RemoveAdminUserButton"; +import { InputField } from "../../../components/forms"; type Props = { adminUsers: string[], t: string => string, - onChange: (boolean, any, string) => void + onChange: (boolean, any, string) => void, + disabled: boolean }; type State = {}; class AdminUserTable extends React.Component<Props, State> { render() { - const { t } = this.props; + const { t, disabled } = this.props; return ( <div> <label className="label">{t("admin-settings.admin-users")}</label> @@ -27,6 +29,7 @@ class AdminUserTable extends React.Component<Props, State> { <RemoveAdminUserButton username={user} removeUser={this.removeUser} + disabled={disabled} /> </td> </tr> diff --git a/scm-ui/src/config/components/table/ProxyExcludesTable.js b/scm-ui/src/config/components/table/ProxyExcludesTable.js index b8102c4f63..ccee16cea5 100644 --- a/scm-ui/src/config/components/table/ProxyExcludesTable.js +++ b/scm-ui/src/config/components/table/ProxyExcludesTable.js @@ -7,7 +7,7 @@ type Props = { proxyExcludes: string[], t: string => string, onChange: (boolean, any, string) => void, - disable: boolean + disabled: boolean }; type State = {}; @@ -28,7 +28,7 @@ class ProxyExcludesTable extends React.Component<Props, State> { <RemoveProxyExcludeButton proxyExcludeName={excludes} removeProxyExclude={this.removeProxyExclude} - disable={this.props.disable} + disabled={this.props.disabled} /> </td> </tr> diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index 5d18f86226..f76a71c11b 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -71,6 +71,7 @@ class GlobalConfig extends React.Component<Props> { submitForm={config => this.modifyConfig(config)} config={config} loading={loading} + configUpdatePermission={configUpdatePermission} /> </div> ); From d9aa04e6206a39e76d135dc3b76163d9ecc8a26a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 13:28:46 +0200 Subject: [PATCH 28/53] add notification message if user has no permission to edit config --- scm-ui/public/locales/en/config.json | 3 +- .../src/config/components/form/ConfigForm.js | 39 ++++++++++++++++--- 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 5f1db2a549..2779b3c28c 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -9,7 +9,8 @@ "error-subtitle": "Unknown Config Error" }, "config-form": { - "submit": "Submit" + "submit": "Submit", + "no-permission-notification": "Please note: You do not have the permission to edit the config!" }, "proxy-settings": { "name": "Proxy Settings", diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index db0a79e7bf..bc4a94b501 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -7,6 +7,7 @@ import ProxySettings from "./ProxySettings"; import GeneralSettings from "./GeneralSettings"; import BaseUrlSettings from "./BaseUrlSettings"; import AdminSettings from "./AdminSettings"; +import Notification from "../../../components/Notification"; type Props = { submitForm: Config => void, @@ -17,7 +18,8 @@ type Props = { }; type State = { - config: Config + config: Config, + showNotification: boolean }; class ConfigForm extends React.Component<Props, State> { @@ -48,15 +50,18 @@ class ConfigForm extends React.Component<Props, State> { enabledXsrfProtection: true, defaultNamespaceStrategy: "", _links: {} - } + }, + showNotification: false }; } componentDidMount() { - const { config } = this.props; - console.log(config); + const { config, configUpdatePermission } = this.props; if (config) { - this.setState({ config: { ...config } }); + this.setState({ ...this.state, config: { ...config } }); + } + if (!configUpdatePermission) { + this.setState({ ...this.state, showNotification: true }); } } @@ -67,9 +72,23 @@ class ConfigForm extends React.Component<Props, State> { render() { const { loading, t, configUpdatePermission } = this.props; - let config = this.state.config; + 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} @@ -132,6 +151,7 @@ class ConfigForm extends React.Component<Props, State> { onChange = (isValid: boolean, changedValue: any, name: string) => { if (isValid) { this.setState({ + ...this.state, config: { ...this.state.config, [name]: changedValue @@ -139,6 +159,13 @@ class ConfigForm extends React.Component<Props, State> { }); } }; + + onClose = () => { + this.setState({ + ...this.state, + showNotification: false + }); + }; } export default translate("config")(ConfigForm); From ce700cde1376bd197ca476e6fd5bd0978b75c3da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 13:30:25 +0200 Subject: [PATCH 29/53] remove unneccesary imports --- .../src/config/components/buttons/RemoveAdminGroupButton.js | 5 +---- .../src/config/components/buttons/RemoveAdminUserButton.js | 1 - scm-ui/src/config/components/table/AdminUserTable.js | 1 - 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js index c593483409..6a819b6972 100644 --- a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js +++ b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js @@ -3,7 +3,6 @@ import React from "react"; import { DeleteButton } from "../../../components/buttons"; import { translate } from "react-i18next"; import classNames from "classnames"; -import {InputField} from "../../../components/forms"; type Props = { t: string => string, @@ -14,11 +13,9 @@ type Props = { type State = {}; - - class RemoveAdminGroupButton extends React.Component<Props, State> { render() { - const { t , groupname, removeGroup, disabled} = this.props; + const { t, groupname, removeGroup, disabled } = this.props; return ( <div className={classNames("is-pulled-right")}> <DeleteButton diff --git a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js index bd4a02a18a..7d79a84030 100644 --- a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js +++ b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js @@ -3,7 +3,6 @@ import React from "react"; import { DeleteButton } from "../../../components/buttons"; import { translate } from "react-i18next"; import classNames from "classnames"; -import { InputField } from "../../../components/forms"; type Props = { t: string => string, diff --git a/scm-ui/src/config/components/table/AdminUserTable.js b/scm-ui/src/config/components/table/AdminUserTable.js index 9a840351e0..caf58f2cc0 100644 --- a/scm-ui/src/config/components/table/AdminUserTable.js +++ b/scm-ui/src/config/components/table/AdminUserTable.js @@ -2,7 +2,6 @@ import React from "react"; import { translate } from "react-i18next"; import RemoveAdminUserButton from "../buttons/RemoveAdminUserButton"; -import { InputField } from "../../../components/forms"; type Props = { adminUsers: string[], From 756da5db1cd67156629dd5f3b387337828227d41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 13:31:04 +0200 Subject: [PATCH 30/53] remove not needed to do --- scm-ui/src/config/containers/GlobalConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index f76a71c11b..a23ef047a6 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -90,7 +90,7 @@ const mapDispatchToProps = dispatch => { }; const mapStateToProps = state => { - const loading = isFetchConfigPending(state) || isModifyConfigPending(state); //TODO: Button lädt so nicht, sondern gesamte Seite + const loading = isFetchConfigPending(state) || isModifyConfigPending(state); const error = getFetchConfigFailure(state) || getModifyConfigFailure(state); const config = getConfig(state); const configUpdatePermission = getConfigUpdatePermission(state); From 322e82380abb66ca7a7fcd21efed3d905c78b8d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 13:33:50 +0200 Subject: [PATCH 31/53] refactoring --- scm-ui/src/config/containers/GlobalConfig.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index a23ef047a6..2f4aa3be22 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -16,7 +16,6 @@ import ErrorPage from "../../components/ErrorPage"; import type { Config } from "../types/Config"; import ConfigForm from "../components/form/ConfigForm"; import Loading from "../../components/Loading"; -import type { User } from "../../users/types/User"; import type { History } from "history"; type Props = { @@ -25,7 +24,7 @@ type Props = { config: Config, configUpdatePermission: boolean, // dispatch functions - modifyConfig: (config: User, callback?: () => void) => void, + modifyConfig: (config: Config, callback?: () => void) => void, // context objects t: string => string, fetchConfig: void => void, @@ -33,7 +32,7 @@ type Props = { }; class GlobalConfig extends React.Component<Props> { - configModified = (config: Config) => () => { + configModified = () => () => { this.props.fetchConfig(); this.props.history.push(`/config`); }; @@ -43,8 +42,7 @@ class GlobalConfig extends React.Component<Props> { } modifyConfig = (config: Config) => { - console.log(config); - this.props.modifyConfig(config, this.configModified(config)); + this.props.modifyConfig(config, this.configModified()); }; render() { From fb56cf4d912c7f588f98089f92e36612ac7ef660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 14:04:27 +0200 Subject: [PATCH 32/53] use password field for proxy password --- scm-ui/src/config/components/form/ProxySettings.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index c21d4e807f..eb434513fe 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -44,6 +44,7 @@ class ProxySettings extends React.Component<Props> { label={t("proxy-settings.proxy-password")} onChange={this.handleProxyPasswordChange} value={proxyPassword} + type="password" disabled={!enableProxy || !hasUpdatePermission} /> <InputField From fb28677a61489e90a74942ed3099f4aac2d8a2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 16:05:24 +0200 Subject: [PATCH 33/53] add validation if number is really a number --- scm-ui/public/locales/en/config.json | 6 ++++ scm-ui/src/components/validation.js | 18 +++++++++++ .../config/components/form/GeneralSettings.js | 31 ++++++++++++++++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 2779b3c28c..8a19b01026 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -55,5 +55,11 @@ "login-attempt-limit-timeout": "Login Attempt Limit Timeout", "enabled-xsrf-protection": "Enabled XSRF Protection", "default-namespace-strategy": "Default Namespace Strategy" + }, + "validation": { + "date-format-invalid": "The date format is not valid", + "login-attempt-limit-timeout-invalid": "This is not a number", + "login-attempt-limit-invalid": "This is not a number", + "plugin-url-invalid": "This is not a valid url" } } diff --git a/scm-ui/src/components/validation.js b/scm-ui/src/components/validation.js index fd61da57a5..0afa016a1e 100644 --- a/scm-ui/src/components/validation.js +++ b/scm-ui/src/components/validation.js @@ -10,3 +10,21 @@ 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); +}; + +const urlRegex = new RegExp( + "^(https?:\\/\\/)?" + // protocol + "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|" + // domain name + "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address + "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path + "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string + "(\\#[-a-z\\d_]*)?$", + "i" +); // fragment locator + +export const isUrlValid = (url: string) => { + return urlRegex.test(url); +}; diff --git a/scm-ui/src/config/components/form/GeneralSettings.js b/scm-ui/src/config/components/form/GeneralSettings.js index 321f247a2a..cb4e4e72b9 100644 --- a/scm-ui/src/config/components/form/GeneralSettings.js +++ b/scm-ui/src/config/components/form/GeneralSettings.js @@ -2,6 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; import { Checkbox, InputField } from "../../../components/forms/index"; +import * as validator from "../../../components/validation"; type Props = { realmDescription: string, @@ -20,7 +21,23 @@ type Props = { hasUpdatePermission: boolean }; -class GeneralSettings extends React.Component<Props> { +type State = { + loginAttemptLimitError: boolean, + loginAttemptLimitTimeoutError: boolean +}; + +class GeneralSettings extends React.Component<Props, State> { + constructor(props: Props) { + super(props); + + this.state = { + loginAttemptLimitError: false, + loginAttemptLimitTimeoutError: false, + baseUrlError: false, + pluginUrlError: false + }; + } + render() { const { t, @@ -75,12 +92,16 @@ class GeneralSettings extends React.Component<Props> { onChange={this.handleLoginAttemptLimitChange} value={loginAttemptLimit} disabled={!hasUpdatePermission} + validationError={this.state.loginAttemptLimitError} + errorMessage={t("validation.login-attempt-limit-invalid")} /> <InputField label={t("general-settings.login-attempt-limit-timeout")} onChange={this.handleLoginAttemptLimitTimeoutChange} value={loginAttemptLimitTimeout} disabled={!hasUpdatePermission} + validationError={this.state.loginAttemptLimitTimeoutError} + errorMessage={t("validation.login-attempt-limit-timeout-invalid")} /> <Checkbox checked={skipFailedAuthenticators} @@ -126,6 +147,10 @@ class GeneralSettings extends React.Component<Props> { this.props.onChange(true, value, "anonymousAccessEnabled"); }; handleLoginAttemptLimitChange = (value: string) => { + this.setState({ + ...this.state, + loginAttemptLimitError: !validator.isNumberValid(value) + }); this.props.onChange(true, value, "loginAttemptLimit"); }; handleSkipFailedAuthenticatorsChange = (value: string) => { @@ -135,6 +160,10 @@ class GeneralSettings extends React.Component<Props> { this.props.onChange(true, value, "pluginUrl"); }; handleLoginAttemptLimitTimeoutChange = (value: string) => { + this.setState({ + ...this.state, + loginAttemptLimitTimeoutError: !validator.isNumberValid(value) + }); this.props.onChange(true, value, "loginAttemptLimitTimeout"); }; handleEnabledXsrfProtectionChange = (value: boolean) => { From 2198db867fc48ab9a7ade13436b994f1bcd6aa59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Thu, 16 Aug 2018 16:41:17 +0200 Subject: [PATCH 34/53] outsourcing of login attemt variables --- scm-ui/public/locales/en/config.json | 7 +- .../src/config/components/form/ConfigForm.js | 42 ++++--- .../config/components/form/GeneralSettings.js | 103 +++++------------- .../config/components/form/LoginAttempt.js | 90 +++++++++++++++ 4 files changed, 149 insertions(+), 93 deletions(-) create mode 100644 scm-ui/src/config/components/form/LoginAttempt.js diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index 8a19b01026..143755a318 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -43,16 +43,19 @@ "add-user-textfield": "Add user you want to add to admin users here", "add-user-button": "Add Admin User" }, + "login-attempt": { + "name": "Login Attempt", + "login-attempt-limit": "Login Attempt Limit", + "login-attempt-limit-timeout": "Login Attempt Limit Timeout" + }, "general-settings": { "realm-description": "Realm Description", "enable-repository-archive": "Enable Repository Archive", "disable-grouping-grid": "Disable Grouping Grid", "date-format": "Date Format", "anonymous-access-enabled": "Anonymous Access Enabled", - "login-attempt-limit": "Login Attempt Limit", "skip-failed-authenticators": "Skip Failed Authenticators", "plugin-url": "Plugin URL", - "login-attempt-limit-timeout": "Login Attempt Limit Timeout", "enabled-xsrf-protection": "Enabled XSRF Protection", "default-namespace-strategy": "Default Namespace Strategy" }, diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index bc4a94b501..430ad6abb2 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -8,6 +8,7 @@ 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, @@ -19,7 +20,8 @@ type Props = { type State = { config: Config, - showNotification: boolean + showNotification: boolean, + loginAttemptError: boolean }; class ConfigForm extends React.Component<Props, State> { @@ -51,7 +53,8 @@ class ConfigForm extends React.Component<Props, State> { defaultNamespaceStrategy: "", _links: {} }, - showNotification: false + showNotification: false, + loginAttemptError: true }; } @@ -95,10 +98,8 @@ class ConfigForm extends React.Component<Props, State> { disableGroupingGrid={config.disableGroupingGrid} dateFormat={config.dateFormat} anonymousAccessEnabled={config.anonymousAccessEnabled} - loginAttemptLimit={config.loginAttemptLimit} skipFailedAuthenticators={config.skipFailedAuthenticators} pluginUrl={config.pluginUrl} - loginAttemptLimitTimeout={config.loginAttemptLimitTimeout} enabledXsrfProtection={config.enabledXsrfProtection} defaultNamespaceStrategy={config.defaultNamespaceStrategy} onChange={(isValid, changedValue, name) => @@ -107,6 +108,15 @@ class ConfigForm extends React.Component<Props, State> { 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} @@ -139,25 +149,27 @@ class ConfigForm extends React.Component<Props, State> { /> <hr /> <SubmitButton - // disabled={!this.isValid()} loading={loading} label={t("config-form.submit")} - disabled={!configUpdatePermission} + disabled={!configUpdatePermission || !this.isValid()} /> </form> ); } + onChange = (isValid: boolean, changedValue: any, name: string) => { - if (isValid) { - this.setState({ - ...this.state, - config: { - ...this.state.config, - [name]: changedValue - } - }); - } + this.setState({ + ...this.state, + config: { + ...this.state.config, + [name]: changedValue + } + }); + }; + + isValid = () => { + return this.state.loginAttemptError; }; onClose = () => { diff --git a/scm-ui/src/config/components/form/GeneralSettings.js b/scm-ui/src/config/components/form/GeneralSettings.js index cb4e4e72b9..5ae999509e 100644 --- a/scm-ui/src/config/components/form/GeneralSettings.js +++ b/scm-ui/src/config/components/form/GeneralSettings.js @@ -2,7 +2,6 @@ import React from "react"; import { translate } from "react-i18next"; import { Checkbox, InputField } from "../../../components/forms/index"; -import * as validator from "../../../components/validation"; type Props = { realmDescription: string, @@ -10,10 +9,8 @@ type Props = { disableGroupingGrid: boolean, dateFormat: string, anonymousAccessEnabled: boolean, - loginAttemptLimit: number, skipFailedAuthenticators: boolean, pluginUrl: string, - loginAttemptLimitTimeout: number, enabledXsrfProtection: boolean, defaultNamespaceStrategy: string, t: string => string, @@ -21,23 +18,7 @@ type Props = { hasUpdatePermission: boolean }; -type State = { - loginAttemptLimitError: boolean, - loginAttemptLimitTimeoutError: boolean -}; - -class GeneralSettings extends React.Component<Props, State> { - constructor(props: Props) { - super(props); - - this.state = { - loginAttemptLimitError: false, - loginAttemptLimitTimeoutError: false, - baseUrlError: false, - pluginUrlError: false - }; - } - +class GeneralSettings extends React.Component<Props> { render() { const { t, @@ -46,10 +27,8 @@ class GeneralSettings extends React.Component<Props, State> { disableGroupingGrid, dateFormat, anonymousAccessEnabled, - loginAttemptLimit, skipFailedAuthenticators, pluginUrl, - loginAttemptLimitTimeout, enabledXsrfProtection, defaultNamespaceStrategy, hasUpdatePermission @@ -63,6 +42,30 @@ class GeneralSettings extends React.Component<Props, State> { 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")} @@ -75,58 +78,18 @@ class GeneralSettings extends React.Component<Props, State> { onChange={this.handleDisableGroupingGridChange} disabled={!hasUpdatePermission} /> - <InputField - label={t("general-settings.date-format")} - onChange={this.handleDateFormatChange} - value={dateFormat} - disabled={!hasUpdatePermission} - /> <Checkbox checked={anonymousAccessEnabled} label={t("general-settings.anonymous-access-enabled")} onChange={this.handleAnonymousAccessEnabledChange} disabled={!hasUpdatePermission} /> - <InputField - label={t("general-settings.login-attempt-limit")} - onChange={this.handleLoginAttemptLimitChange} - value={loginAttemptLimit} - disabled={!hasUpdatePermission} - validationError={this.state.loginAttemptLimitError} - errorMessage={t("validation.login-attempt-limit-invalid")} - /> - <InputField - label={t("general-settings.login-attempt-limit-timeout")} - onChange={this.handleLoginAttemptLimitTimeoutChange} - value={loginAttemptLimitTimeout} - disabled={!hasUpdatePermission} - validationError={this.state.loginAttemptLimitTimeoutError} - errorMessage={t("validation.login-attempt-limit-timeout-invalid")} - /> <Checkbox checked={skipFailedAuthenticators} label={t("general-settings.skip-failed-authenticators")} onChange={this.handleSkipFailedAuthenticatorsChange} disabled={!hasUpdatePermission} /> - <InputField - label={t("general-settings.plugin-url")} - onChange={this.handlePluginUrlChange} - value={pluginUrl} - disabled={!hasUpdatePermission} - /> - <Checkbox - checked={enabledXsrfProtection} - label={t("general-settings.enabled-xsrf-protection")} - onChange={this.handleEnabledXsrfProtectionChange} - disabled={!hasUpdatePermission} - /> - <InputField - label={t("general-settings.default-namespace-strategy")} - onChange={this.handleDefaultNamespaceStrategyChange} - value={defaultNamespaceStrategy} - disabled={!hasUpdatePermission} - /> </div> ); } @@ -146,26 +109,14 @@ class GeneralSettings extends React.Component<Props, State> { handleAnonymousAccessEnabledChange = (value: string) => { this.props.onChange(true, value, "anonymousAccessEnabled"); }; - handleLoginAttemptLimitChange = (value: string) => { - this.setState({ - ...this.state, - loginAttemptLimitError: !validator.isNumberValid(value) - }); - this.props.onChange(true, value, "loginAttemptLimit"); - }; + handleSkipFailedAuthenticatorsChange = (value: string) => { this.props.onChange(true, value, "skipFailedAuthenticators"); }; handlePluginUrlChange = (value: string) => { this.props.onChange(true, value, "pluginUrl"); }; - handleLoginAttemptLimitTimeoutChange = (value: string) => { - this.setState({ - ...this.state, - loginAttemptLimitTimeoutError: !validator.isNumberValid(value) - }); - this.props.onChange(true, value, "loginAttemptLimitTimeout"); - }; + handleEnabledXsrfProtectionChange = (value: boolean) => { this.props.onChange(true, value, "enabledXsrfProtection"); }; diff --git a/scm-ui/src/config/components/form/LoginAttempt.js b/scm-ui/src/config/components/form/LoginAttempt.js new file mode 100644 index 0000000000..489828fd0b --- /dev/null +++ b/scm-ui/src/config/components/form/LoginAttempt.js @@ -0,0 +1,90 @@ +// @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(this.loginAttemptIsValid(), value, "loginAttemptLimit"); + }; + + handleLoginAttemptLimitTimeoutChange = (value: string) => { + this.setState({ + ...this.state, + loginAttemptLimitTimeoutError: !validator.isNumberValid(value) + }); + this.props.onChange( + this.loginAttemptIsValid(), + value, + "loginAttemptLimitTimeout" + ); + }; + + loginAttemptIsValid = () => { + return ( + this.state.loginAttemptLimitError || + this.state.loginAttemptLimitTimeoutError + ); + }; +} + +export default translate("config")(LoginAttempt); From 98b8e343085a1ad87053c1ee382f21e37e8a7c67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 08:07:09 +0200 Subject: [PATCH 35/53] remove unnecessary url --- scm-ui/src/components/validation.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/scm-ui/src/components/validation.js b/scm-ui/src/components/validation.js index 0afa016a1e..7fa4a262b9 100644 --- a/scm-ui/src/components/validation.js +++ b/scm-ui/src/components/validation.js @@ -14,17 +14,3 @@ export const isMailValid = (mail: string) => { export const isNumberValid = (number: string) => { return !isNaN(number); }; - -const urlRegex = new RegExp( - "^(https?:\\/\\/)?" + // protocol - "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|" + // domain name - "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address - "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path - "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string - "(\\#[-a-z\\d_]*)?$", - "i" -); // fragment locator - -export const isUrlValid = (url: string) => { - return urlRegex.test(url); -}; From c0c44ec22c1101438065c28200de014b586216ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 08:40:49 +0200 Subject: [PATCH 36/53] add tests for number validation --- scm-ui/src/components/validation.test.js | 15 +++++++++ .../src/config/components/form/ConfigForm.js | 33 +++++++++++++++---- .../config/components/form/LoginAttempt.js | 15 ++++----- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/scm-ui/src/components/validation.test.js b/scm-ui/src/components/validation.test.js index 74b9debd28..c264b20a1c 100644 --- a/scm-ui/src/components/validation.test.js +++ b/scm-ui/src/components/validation.test.js @@ -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); + } + }); +}); diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index 430ad6abb2..0dbd4e83ba 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -21,7 +21,10 @@ type Props = { type State = { config: Config, showNotification: boolean, - loginAttemptError: boolean + error: { + loginAttemptLimitTimeout: boolean, + loginAttemptLimit: boolean + } }; class ConfigForm extends React.Component<Props, State> { @@ -54,7 +57,10 @@ class ConfigForm extends React.Component<Props, State> { _links: {} }, showNotification: false, - loginAttemptError: true + error: { + loginAttemptLimitTimeout: false, + loginAttemptLimit: false + } }; } @@ -151,25 +157,40 @@ class ConfigForm extends React.Component<Props, State> { <SubmitButton loading={loading} label={t("config-form.submit")} - disabled={!configUpdatePermission || !this.isValid()} + 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 } }); }; - isValid = () => { - return this.state.loginAttemptError; + hasError = () => { + console.log("loginAttemtLimit " + this.state.error.loginAttemptLimit); + console.log( + "loginAttemtLimitTimeout " + this.state.error.loginAttemptLimitTimeout + ); + + console.log( + this.state.error.loginAttemptLimit || + this.state.error.loginAttemptLimitTimeout + ); + return ( + this.state.error.loginAttemptLimit || + this.state.error.loginAttemptLimitTimeout + ); }; onClose = () => { diff --git a/scm-ui/src/config/components/form/LoginAttempt.js b/scm-ui/src/config/components/form/LoginAttempt.js index 489828fd0b..da3a4ce0da 100644 --- a/scm-ui/src/config/components/form/LoginAttempt.js +++ b/scm-ui/src/config/components/form/LoginAttempt.js @@ -64,7 +64,11 @@ class LoginAttempt extends React.Component<Props, State> { ...this.state, loginAttemptLimitError: !validator.isNumberValid(value) }); - this.props.onChange(this.loginAttemptIsValid(), value, "loginAttemptLimit"); + this.props.onChange( + validator.isNumberValid(value), + value, + "loginAttemptLimit" + ); }; handleLoginAttemptLimitTimeoutChange = (value: string) => { @@ -73,18 +77,11 @@ class LoginAttempt extends React.Component<Props, State> { loginAttemptLimitTimeoutError: !validator.isNumberValid(value) }); this.props.onChange( - this.loginAttemptIsValid(), + validator.isNumberValid(value), value, "loginAttemptLimitTimeout" ); }; - - loginAttemptIsValid = () => { - return ( - this.state.loginAttemptLimitError || - this.state.loginAttemptLimitTimeoutError - ); - }; } export default translate("config")(LoginAttempt); From 3071cab9b7fa719190677fe073823fc6e768e619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 08:57:25 +0200 Subject: [PATCH 37/53] remove unused console.log --- scm-ui/src/config/components/form/ConfigForm.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index 0dbd4e83ba..f859eaf07d 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -178,11 +178,6 @@ class ConfigForm extends React.Component<Props, State> { }; hasError = () => { - console.log("loginAttemtLimit " + this.state.error.loginAttemptLimit); - console.log( - "loginAttemtLimitTimeout " + this.state.error.loginAttemptLimitTimeout - ); - console.log( this.state.error.loginAttemptLimit || this.state.error.loginAttemptLimitTimeout From 7efb082c43536edd254640f6120c0bac5137271f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 09:17:15 +0200 Subject: [PATCH 38/53] reset error when loading config again --- scm-ui/src/config/containers/GlobalConfig.js | 8 +++++++- scm-ui/src/config/modules/config.js | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index 2f4aa3be22..ee02da9709 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -9,7 +9,8 @@ import { modifyConfig, isModifyConfigPending, getConfigUpdatePermission, - getModifyConfigFailure + getModifyConfigFailure, + modifyConfigReset } from "../modules/config"; import connect from "react-redux/es/connect/connect"; import ErrorPage from "../../components/ErrorPage"; @@ -28,6 +29,7 @@ type Props = { // context objects t: string => string, fetchConfig: void => void, + configReset: void => void, history: History }; @@ -38,6 +40,7 @@ class GlobalConfig extends React.Component<Props> { }; componentDidMount() { + this.props.configReset(); this.props.fetchConfig(); } @@ -83,6 +86,9 @@ const mapDispatchToProps = dispatch => { }, modifyConfig: (config: Config, callback?: () => void) => { dispatch(modifyConfig(config, callback)); + }, + configReset: () => { + dispatch(modifyConfigReset()); } }; }; diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 4534dbaeb6..45c75348b6 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -16,6 +16,7 @@ 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"; @@ -66,7 +67,7 @@ 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) + .put(config._links.update.href + "letsfail!", config, CONTENT_TYPE_CONFIG) .then(() => { dispatch(modifyConfigSuccess(config)); if (callback) { @@ -108,6 +109,12 @@ export function modifyConfigFailure(config: Config, error: Error): Action { }; } +export function modifyConfigReset() { + return { + type: MODIFY_CONFIG_RESET + }; +} + //reducer function reducer(state: any = {}, action: any = {}) { From 7d100edd3df4e21b84ae7f983ef44509d155ab65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 10:48:34 +0200 Subject: [PATCH 39/53] repair false modify url --- scm-ui/src/config/modules/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 45c75348b6..6fe1594236 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -67,7 +67,7 @@ export function modifyConfig(config: Config, callback?: () => void) { return function(dispatch: Dispatch) { dispatch(modifyConfigPending(config)); return apiClient - .put(config._links.update.href + "letsfail!", config, CONTENT_TYPE_CONFIG) + .put(config._links.update.href, config, CONTENT_TYPE_CONFIG) .then(() => { dispatch(modifyConfigSuccess(config)); if (callback) { From cd0c1a5f2a35a046c986c75f4c4a5e7d3778e15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:25:47 +0200 Subject: [PATCH 40/53] remove unnecessary console.log --- scm-ui/src/config/components/form/ConfigForm.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index f859eaf07d..afb6b243d6 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -178,10 +178,6 @@ class ConfigForm extends React.Component<Props, State> { }; hasError = () => { - console.log( - this.state.error.loginAttemptLimit || - this.state.error.loginAttemptLimitTimeout - ); return ( this.state.error.loginAttemptLimit || this.state.error.loginAttemptLimitTimeout From 68f62b9b053f7e37421bd448a7166bd19db334d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:27:35 +0200 Subject: [PATCH 41/53] transfer remove button of each component to a global remove button --- scm-ui/src/components/buttons/index.js | 1 + .../buttons/RemoveAdminGroupButton.js | 34 ------------------ .../buttons/RemoveAdminUserButton.js | 34 ------------------ .../buttons/RemoveProxyExcludeButton.js | 36 ------------------- .../components/table/AdminGroupTable.js | 11 +++--- .../config/components/table/AdminUserTable.js | 11 +++--- .../components/table/ProxyExcludesTable.js | 11 +++--- 7 files changed, 19 insertions(+), 119 deletions(-) delete mode 100644 scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js delete mode 100644 scm-ui/src/config/components/buttons/RemoveAdminUserButton.js delete mode 100644 scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js diff --git a/scm-ui/src/components/buttons/index.js b/scm-ui/src/components/buttons/index.js index 8dfc6e00ef..e0d94d29b9 100644 --- a/scm-ui/src/components/buttons/index.js +++ b/scm-ui/src/components/buttons/index.js @@ -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"; diff --git a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js b/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js deleted file mode 100644 index 6a819b6972..0000000000 --- a/scm-ui/src/config/components/buttons/RemoveAdminGroupButton.js +++ /dev/null @@ -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, - groupname: string, - removeGroup: string => void, - disabled: boolean -}; - -type State = {}; - -class RemoveAdminGroupButton extends React.Component<Props, State> { - render() { - const { t, groupname, removeGroup, disabled } = this.props; - return ( - <div className={classNames("is-pulled-right")}> - <DeleteButton - label={t("admin-settings.remove-group-button")} - action={(event: Event) => { - event.preventDefault(); - removeGroup(groupname); - }} - disabled={disabled} - /> - </div> - ); - } -} - -export default translate("config")(RemoveAdminGroupButton); diff --git a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js b/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js deleted file mode 100644 index 7d79a84030..0000000000 --- a/scm-ui/src/config/components/buttons/RemoveAdminUserButton.js +++ /dev/null @@ -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, - username: string, - removeUser: string => void, - disabled: boolean -}; - -type State = {}; - -class RemoveAdminUserButton extends React.Component<Props, State> { - render() { - const { t, username, removeUser, disabled } = this.props; - return ( - <div className={classNames("is-pulled-right")}> - <DeleteButton - label={t("admin-settings.remove-user-button")} - action={(event: Event) => { - event.preventDefault(); - removeUser(username); - }} - disabled={disabled} - /> - </div> - ); - } -} - -export default translate("config")(RemoveAdminUserButton); diff --git a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js b/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js deleted file mode 100644 index acdfd6292a..0000000000 --- a/scm-ui/src/config/components/buttons/RemoveProxyExcludeButton.js +++ /dev/null @@ -1,36 +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, - proxyExcludeName: string, - removeProxyExclude: string => void, - disabled: boolean -}; - -type State = {}; - - - -class RemoveProxyExcludeButton extends React.Component<Props, State> { - render() { - const { t , proxyExcludeName, removeProxyExclude} = this.props; - return ( - <div className={classNames("is-pulled-right")}> - <DeleteButton - label={t("proxy-settings.remove-proxy-exclude-button")} - action={(event: Event) => { - event.preventDefault(); - removeProxyExclude(proxyExcludeName); - }} - disabled={this.props.disabled} - /> - </div> - ); - } -} - -export default translate("config")(RemoveProxyExcludeButton); diff --git a/scm-ui/src/config/components/table/AdminGroupTable.js b/scm-ui/src/config/components/table/AdminGroupTable.js index d5566e7ce1..fc05c75f06 100644 --- a/scm-ui/src/config/components/table/AdminGroupTable.js +++ b/scm-ui/src/config/components/table/AdminGroupTable.js @@ -1,7 +1,7 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import RemoveAdminGroupButton from "../buttons/RemoveAdminGroupButton"; +import { RemoveEntryOfTableButton } from "../../../components/buttons"; type Props = { adminGroups: string[], @@ -25,10 +25,11 @@ class AdminGroupTable extends React.Component<Props, State> { <tr key={group}> <td key={group}>{group}</td> <td> - <RemoveAdminGroupButton - groupname={group} - removeGroup={this.removeGroup} + <RemoveEntryOfTableButton + entryname={group} + removeEntry={this.removeEntry} disabled={disabled} + label={t("admin-settings.remove-group-button")} /> </td> </tr> @@ -40,7 +41,7 @@ class AdminGroupTable extends React.Component<Props, State> { ); } - removeGroup = (groupname: string) => { + removeEntry = (groupname: string) => { const newGroups = this.props.adminGroups.filter(name => name !== groupname); this.props.onChange(true, newGroups, "adminGroups"); }; diff --git a/scm-ui/src/config/components/table/AdminUserTable.js b/scm-ui/src/config/components/table/AdminUserTable.js index caf58f2cc0..c622a0e027 100644 --- a/scm-ui/src/config/components/table/AdminUserTable.js +++ b/scm-ui/src/config/components/table/AdminUserTable.js @@ -1,7 +1,7 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import RemoveAdminUserButton from "../buttons/RemoveAdminUserButton"; +import { RemoveEntryOfTableButton } from "../../../components/buttons"; type Props = { adminUsers: string[], @@ -25,10 +25,11 @@ class AdminUserTable extends React.Component<Props, State> { <tr key={user}> <td key={user}>{user}</td> <td> - <RemoveAdminUserButton - username={user} - removeUser={this.removeUser} + <RemoveEntryOfTableButton + entryname={user} + removeEntry={this.removeEntry} disabled={disabled} + label={t("admin-settings.remove-user-button")} /> </td> </tr> @@ -40,7 +41,7 @@ class AdminUserTable extends React.Component<Props, State> { ); } - removeUser = (username: string) => { + removeEntry = (username: string) => { const newUsers = this.props.adminUsers.filter(name => name !== username); this.props.onChange(true, newUsers, "adminUsers"); }; diff --git a/scm-ui/src/config/components/table/ProxyExcludesTable.js b/scm-ui/src/config/components/table/ProxyExcludesTable.js index ccee16cea5..4476442c48 100644 --- a/scm-ui/src/config/components/table/ProxyExcludesTable.js +++ b/scm-ui/src/config/components/table/ProxyExcludesTable.js @@ -1,7 +1,7 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import RemoveProxyExcludeButton from "../buttons/RemoveProxyExcludeButton"; +import { RemoveEntryOfTableButton } from "../../../components/buttons"; type Props = { proxyExcludes: string[], @@ -25,10 +25,11 @@ class ProxyExcludesTable extends React.Component<Props, State> { <tr key={excludes}> <td key={excludes}>{excludes}</td> <td> - <RemoveProxyExcludeButton - proxyExcludeName={excludes} - removeProxyExclude={this.removeProxyExclude} + <RemoveEntryOfTableButton + entryname={excludes} + removeEntry={this.removeEntry} disabled={this.props.disabled} + label={t("proxy-settings.remove-proxy-exclude-button")} /> </td> </tr> @@ -40,7 +41,7 @@ class ProxyExcludesTable extends React.Component<Props, State> { ); } - removeProxyExclude = (excludename: string) => { + removeEntry = (excludename: string) => { const newExcludes = this.props.proxyExcludes.filter( name => name !== excludename ); From d5815142c0d3296bcc83cb0d789d505cddde8e97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:28:01 +0200 Subject: [PATCH 42/53] add global remove button for table entries --- .../buttons/RemoveEntryOfTableButton.js | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 scm-ui/src/components/buttons/RemoveEntryOfTableButton.js diff --git a/scm-ui/src/components/buttons/RemoveEntryOfTableButton.js b/scm-ui/src/components/buttons/RemoveEntryOfTableButton.js new file mode 100644 index 0000000000..280226b938 --- /dev/null +++ b/scm-ui/src/components/buttons/RemoveEntryOfTableButton.js @@ -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; From d8e4c7d63ac0894d9ffc8665812e2c0cc01bec68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:31:36 +0200 Subject: [PATCH 43/53] use global remove button for table entries in groups --- .../src/groups/components/MemberNameTable.js | 12 ++++--- .../components/buttons/RemoveMemberButton.js | 34 ------------------- 2 files changed, 7 insertions(+), 39 deletions(-) delete mode 100644 scm-ui/src/groups/components/buttons/RemoveMemberButton.js diff --git a/scm-ui/src/groups/components/MemberNameTable.js b/scm-ui/src/groups/components/MemberNameTable.js index 699c009413..0ae20da7d2 100644 --- a/scm-ui/src/groups/components/MemberNameTable.js +++ b/scm-ui/src/groups/components/MemberNameTable.js @@ -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); }; diff --git a/scm-ui/src/groups/components/buttons/RemoveMemberButton.js b/scm-ui/src/groups/components/buttons/RemoveMemberButton.js deleted file mode 100644 index 40c7b39cc0..0000000000 --- a/scm-ui/src/groups/components/buttons/RemoveMemberButton.js +++ /dev/null @@ -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); From d9e66fdbaa120921de62cf9893244f6808df33cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:33:48 +0200 Subject: [PATCH 44/53] delete of unnecessary comments --- .../components/fields/AddAdminGroupField.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/scm-ui/src/config/components/fields/AddAdminGroupField.js b/scm-ui/src/config/components/fields/AddAdminGroupField.js index 0991056b66..21cba365ed 100644 --- a/scm-ui/src/config/components/fields/AddAdminGroupField.js +++ b/scm-ui/src/config/components/fields/AddAdminGroupField.js @@ -12,16 +12,14 @@ type Props = { }; type State = { - groupToAdd: string, - //validationError: boolean + groupToAdd: string }; class AddAdminGroupField extends React.Component<Props, State> { constructor(props) { super(props); this.state = { - groupToAdd: "", - //validationError: false + groupToAdd: "" }; } @@ -30,7 +28,6 @@ class AddAdminGroupField extends React.Component<Props, State> { return ( <div className="field"> <InputField - label={t("admin-settings.add-group-textfield")} errorMessage={t("admin-settings.add-group-error")} onChange={this.handleAddGroupChange} @@ -43,7 +40,6 @@ class AddAdminGroupField extends React.Component<Props, State> { label={t("admin-settings.add-group-button")} action={this.addButtonClicked} disabled={disabled} - //disabled={!isMemberNameValid(this.state.memberToAdd)} /> </div> ); @@ -56,17 +52,14 @@ class AddAdminGroupField extends React.Component<Props, State> { appendGroup = () => { const { groupToAdd } = this.state; - //if (isMemberNameValid(memberToAdd)) { - this.props.addGroup(groupToAdd); - this.setState({ ...this.state, groupToAdd: "" }); - // } + this.props.addGroup(groupToAdd); + this.setState({ ...this.state, groupToAdd: "" }); }; handleAddGroupChange = (groupname: string) => { this.setState({ ...this.state, - groupToAdd: groupname, - //validationError: membername.length > 0 && !isMemberNameValid(membername) + groupToAdd: groupname }); }; } From ebed0d0997a9d9cf70e88371a535a0bdcac0f686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 11:59:12 +0200 Subject: [PATCH 45/53] transfer field to add new entry of each component to global one --- .../components/forms/AddEntryToTableField.js | 68 +++++++++++++++++ .../components/fields/AddAdminGroupField.js | 67 ----------------- .../components/fields/AddAdminUserField.js | 73 ------------------- .../components/fields/AddProxyExcludeField.js | 73 ------------------- .../config/components/form/AdminSettings.js | 18 ++++- .../config/components/form/ProxySettings.js | 10 ++- .../src/groups/components/AddMemberField.js | 71 ------------------ scm-ui/src/groups/components/GroupForm.js | 10 ++- 8 files changed, 97 insertions(+), 293 deletions(-) create mode 100644 scm-ui/src/components/forms/AddEntryToTableField.js delete mode 100644 scm-ui/src/config/components/fields/AddAdminGroupField.js delete mode 100644 scm-ui/src/config/components/fields/AddAdminUserField.js delete mode 100644 scm-ui/src/config/components/fields/AddProxyExcludeField.js delete mode 100644 scm-ui/src/groups/components/AddMemberField.js diff --git a/scm-ui/src/components/forms/AddEntryToTableField.js b/scm-ui/src/components/forms/AddEntryToTableField.js new file mode 100644 index 0000000000..1770e07807 --- /dev/null +++ b/scm-ui/src/components/forms/AddEntryToTableField.js @@ -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; diff --git a/scm-ui/src/config/components/fields/AddAdminGroupField.js b/scm-ui/src/config/components/fields/AddAdminGroupField.js deleted file mode 100644 index 21cba365ed..0000000000 --- a/scm-ui/src/config/components/fields/AddAdminGroupField.js +++ /dev/null @@ -1,67 +0,0 @@ -//@flow -import React from "react"; - -import { translate } from "react-i18next"; -import { AddButton } from "../../../components/buttons"; -import InputField from "../../../components/forms/InputField"; - -type Props = { - t: string => string, - addGroup: string => void, - disabled: boolean -}; - -type State = { - groupToAdd: string -}; - -class AddAdminGroupField extends React.Component<Props, State> { - constructor(props) { - super(props); - this.state = { - groupToAdd: "" - }; - } - - render() { - const { t, disabled } = this.props; - return ( - <div className="field"> - <InputField - label={t("admin-settings.add-group-textfield")} - errorMessage={t("admin-settings.add-group-error")} - onChange={this.handleAddGroupChange} - validationError={false} - value={this.state.groupToAdd} - onReturnPressed={this.appendGroup} - disabled={disabled} - /> - <AddButton - label={t("admin-settings.add-group-button")} - action={this.addButtonClicked} - disabled={disabled} - /> - </div> - ); - } - - addButtonClicked = (event: Event) => { - event.preventDefault(); - this.appendGroup(); - }; - - appendGroup = () => { - const { groupToAdd } = this.state; - this.props.addGroup(groupToAdd); - this.setState({ ...this.state, groupToAdd: "" }); - }; - - handleAddGroupChange = (groupname: string) => { - this.setState({ - ...this.state, - groupToAdd: groupname - }); - }; -} - -export default translate("config")(AddAdminGroupField); diff --git a/scm-ui/src/config/components/fields/AddAdminUserField.js b/scm-ui/src/config/components/fields/AddAdminUserField.js deleted file mode 100644 index 025ed6cb4d..0000000000 --- a/scm-ui/src/config/components/fields/AddAdminUserField.js +++ /dev/null @@ -1,73 +0,0 @@ -//@flow -import React from "react"; - -import { translate } from "react-i18next"; -import { AddButton } from "../../../components/buttons"; -import InputField from "../../../components/forms/InputField"; - -type Props = { - t: string => string, - addUser: string => void, - disabled: boolean -}; - -type State = { - userToAdd: string - //validationError: boolean -}; - -class AddAdminUserField extends React.Component<Props, State> { - constructor(props) { - super(props); - this.state = { - userToAdd: "" - //validationError: false - }; - } - - render() { - const { t, disabled } = this.props; - return ( - <div className="field"> - <InputField - label={t("admin-settings.add-user-textfield")} - errorMessage={t("admin-settings.add-user-error")} - onChange={this.handleAddUserChange} - validationError={false} - value={this.state.userToAdd} - onReturnPressed={this.appendUser} - disabled={disabled} - /> - <AddButton - label={t("admin-settings.add-user-button")} - action={this.addButtonClicked} - disabled={disabled} - //disabled={!isMemberNameValid(this.state.memberToAdd)} - /> - </div> - ); - } - - addButtonClicked = (event: Event) => { - event.preventDefault(); - this.appendUser(); - }; - - appendUser = () => { - const { userToAdd } = this.state; - //if (isMemberNameValid(memberToAdd)) { - this.props.addUser(userToAdd); - this.setState({ ...this.state, userToAdd: "" }); - // } - }; - - handleAddUserChange = (username: string) => { - this.setState({ - ...this.state, - userToAdd: username - //validationError: membername.length > 0 && !isMemberNameValid(membername) - }); - }; -} - -export default translate("config")(AddAdminUserField); diff --git a/scm-ui/src/config/components/fields/AddProxyExcludeField.js b/scm-ui/src/config/components/fields/AddProxyExcludeField.js deleted file mode 100644 index bb7e005b02..0000000000 --- a/scm-ui/src/config/components/fields/AddProxyExcludeField.js +++ /dev/null @@ -1,73 +0,0 @@ -//@flow -import React from "react"; - -import { translate } from "react-i18next"; -import { AddButton } from "../../../components/buttons"; -import InputField from "../../../components/forms/InputField"; - -type Props = { - t: string => string, - addProxyExclude: string => void, - disabled: boolean -}; - -type State = { - proxyExcludeToAdd: string - //validationError: boolean -}; - -class AddProxyExcludeField extends React.Component<Props, State> { - constructor(props) { - super(props); - this.state = { - proxyExcludeToAdd: "" - //validationError: false - }; - } - - render() { - const { t } = this.props; - return ( - <div className="field"> - <InputField - label={t("proxy-settings.add-proxy-exclude-textfield")} - errorMessage={t("proxy-settings.add-proxy-exclude-error")} - onChange={this.handleAddProxyExcludeChange} - validationError={false} - value={this.state.proxyExcludeToAdd} - onReturnPressed={this.appendProxyExclude} - disabled={this.props.disabled} - /> - <AddButton - label={t("proxy-settings.add-proxy-exclude-button")} - action={this.addButtonClicked} - disabled={this.props.disabled} - //disabled={!isMemberNameValid(this.state.memberToAdd)} - /> - </div> - ); - } - - addButtonClicked = (event: Event) => { - event.preventDefault(); - this.appendProxyExclude(); - }; - - appendProxyExclude = () => { - const { proxyExcludeToAdd } = this.state; - //if (isMemberNameValid(memberToAdd)) { - this.props.addProxyExclude(proxyExcludeToAdd); - this.setState({ ...this.state, proxyExcludeToAdd: "" }); - // } - }; - - handleAddProxyExcludeChange = (username: string) => { - this.setState({ - ...this.state, - proxyExcludeToAdd: username - //validationError: membername.length > 0 && !isMemberNameValid(membername) - }); - }; -} - -export default translate("config")(AddProxyExcludeField); diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index b7a39f234c..ea1a88fe6f 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -4,8 +4,7 @@ import { translate } from "react-i18next"; import Subtitle from "../../../components/layout/Subtitle"; import AdminGroupTable from "../table/AdminGroupTable"; import AdminUserTable from "../table/AdminUserTable"; -import AddAdminGroupField from "../fields/AddAdminGroupField"; -import AddAdminUserField from "../fields/AddAdminUserField"; +import AddEntryToTableField from "../../../components/forms/AddEntryToTableField"; type Props = { adminGroups: string[], @@ -29,8 +28,14 @@ class AdminSettings extends React.Component<Props> { } disabled={!hasUpdatePermission} /> - <AddAdminGroupField addGroup={this.addGroup} 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) => @@ -38,7 +43,12 @@ class AdminSettings extends React.Component<Props> { } disabled={!hasUpdatePermission} /> - <AddAdminUserField addUser={this.addUser} 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> ); diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index eb434513fe..a1d513f9b1 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -4,7 +4,7 @@ import { translate } from "react-i18next"; import { Checkbox, InputField } from "../../../components/forms/index"; import Subtitle from "../../../components/layout/Subtitle"; import ProxyExcludesTable from "../table/ProxyExcludesTable"; -import AddProxyExcludeField from "../fields/AddProxyExcludeField"; +import AddEntryToTableField from "../../../components/forms/AddEntryToTableField"; type Props = { proxyPassword: string, @@ -65,6 +65,7 @@ class ProxySettings extends React.Component<Props> { onChange={this.handleProxyUserChange} disabled={!enableProxy || !hasUpdatePermission} /> + <ProxyExcludesTable proxyExcludes={proxyExcludes} onChange={(isValid, changedValue, name) => @@ -72,9 +73,12 @@ class ProxySettings extends React.Component<Props> { } disabled={!enableProxy || !hasUpdatePermission} /> - <AddProxyExcludeField - addProxyExclude={this.addProxyExclude} + <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> ); diff --git a/scm-ui/src/groups/components/AddMemberField.js b/scm-ui/src/groups/components/AddMemberField.js deleted file mode 100644 index 6237e88291..0000000000 --- a/scm-ui/src/groups/components/AddMemberField.js +++ /dev/null @@ -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); diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js index 2e941ea7eb..a989bdc1c5 100644 --- a/scm-ui/src/groups/components/GroupForm.js +++ b/scm-ui/src/groups/components/GroupForm.js @@ -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")} From 18bf13a6af73b4df210537c88053360f7047a5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@cloudogu.com> Date: Tue, 21 Aug 2018 13:06:52 +0200 Subject: [PATCH 46/53] formatting --- scm-ui/src/config/components/form/AdminSettings.js | 1 - scm-ui/src/config/components/form/ProxySettings.js | 1 - 2 files changed, 2 deletions(-) diff --git a/scm-ui/src/config/components/form/AdminSettings.js b/scm-ui/src/config/components/form/AdminSettings.js index ea1a88fe6f..98ab7350fb 100644 --- a/scm-ui/src/config/components/form/AdminSettings.js +++ b/scm-ui/src/config/components/form/AdminSettings.js @@ -35,7 +35,6 @@ class AdminSettings extends React.Component<Props> { fieldLabel={t("admin-settings.add-group-textfield")} errorMessage={t("admin-settings.add-group-error")} /> - <AdminUserTable adminUsers={adminUsers} onChange={(isValid, changedValue, name) => diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index a1d513f9b1..42cd0ab228 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -65,7 +65,6 @@ class ProxySettings extends React.Component<Props> { onChange={this.handleProxyUserChange} disabled={!enableProxy || !hasUpdatePermission} /> - <ProxyExcludesTable proxyExcludes={proxyExcludes} onChange={(isValid, changedValue, name) => From 6f5789968705b9b692bf22bc17f19fb851ffeca7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= <maren.suewer@web.de> Date: Tue, 21 Aug 2018 11:13:22 +0000 Subject: [PATCH 47/53] scm.iml edited online with Bitbucket --- scm.iml | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 scm.iml diff --git a/scm.iml b/scm.iml deleted file mode 100644 index 20f9f4b564..0000000000 --- a/scm.iml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4"> - <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8"> - <output url="file://$MODULE_DIR$/target/classes" /> - <output-test url="file://$MODULE_DIR$/target/test-classes" /> - <content url="file://$MODULE_DIR$"> - <excludeFolder url="file://$MODULE_DIR$/target" /> - </content> - <orderEntry type="inheritedJdk" /> - <orderEntry type="sourceFolder" forTests="false" /> - <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" /> - <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:1.3" level="project" /> - <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-library:1.3" level="project" /> - <orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-all:1.10.19" level="project" /> - <orderEntry type="library" scope="TEST" name="Maven: org.assertj:assertj-core:3.10.0" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.github.cloudogu:ces-build-lib:9aadeeb" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.cloudbees:groovy-cps:1.21" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.guava:guava:11.0.1" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: com.google.code.findbugs:jsr305:1.3.9" level="project" /> - <orderEntry type="library" scope="PROVIDED" name="Maven: org.codehaus.groovy:groovy-all:2.4.11" level="project" /> - </component> -</module> \ No newline at end of file From dca8fcbde197ea1a180ea533f6cdfa0cb57820b2 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 09:46:03 +0200 Subject: [PATCH 48/53] fix wrong comment ordering --- scm-ui/src/config/containers/GlobalConfig.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index ee02da9709..e1f6793fb1 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -24,12 +24,14 @@ type Props = { error: Error, config: Config, configUpdatePermission: boolean, + // dispatch functions modifyConfig: (config: Config, callback?: () => void) => void, - // context objects - t: string => string, fetchConfig: void => void, configReset: void => void, + + // context objects + t: string => string, history: History }; From eb207cecfddbdf79d427dd6c4378a8fab4d64af8 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 09:47:33 +0200 Subject: [PATCH 49/53] fix errors with null values for array types --- scm-ui/src/config/modules/config.js | 16 +++++++++- scm-ui/src/config/modules/config.test.js | 37 ++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 6fe1594236..5cf331c324 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -117,12 +117,26 @@ export function modifyConfigReset() { //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 FETCH_CONFIG_SUCCESS: + const config = removeNullValues(action.payload); return { ...state, - entries: action.payload, + entries: config, configUpdatePermission: action.payload._links.update ? true : false }; default: diff --git a/scm-ui/src/config/modules/config.test.js b/scm-ui/src/config/modules/config.test.js index d991bd929a..09d794dcb0 100644 --- a/scm-ui/src/config/modules/config.test.js +++ b/scm-ui/src/config/modules/config.test.js @@ -56,6 +56,35 @@ const 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 @@ -175,6 +204,14 @@ describe("config reducer", () => { 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", () => { From e3b6b6c18fff1e96ce044845433ea317f6ffa5d8 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 10:10:41 +0200 Subject: [PATCH 50/53] remove unnecessary fetches --- scm-ui/src/config/containers/GlobalConfig.js | 11 ++--------- scm-ui/src/config/modules/config.js | 1 + 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index e1f6793fb1..3d8858daf9 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -17,7 +17,6 @@ import ErrorPage from "../../components/ErrorPage"; import type { Config } from "../types/Config"; import ConfigForm from "../components/form/ConfigForm"; import Loading from "../../components/Loading"; -import type { History } from "history"; type Props = { loading: boolean, @@ -31,23 +30,17 @@ type Props = { configReset: void => void, // context objects - t: string => string, - history: History + t: string => string }; class GlobalConfig extends React.Component<Props> { - configModified = () => () => { - this.props.fetchConfig(); - this.props.history.push(`/config`); - }; - componentDidMount() { this.props.configReset(); this.props.fetchConfig(); } modifyConfig = (config: Config) => { - this.props.modifyConfig(config, this.configModified()); + this.props.modifyConfig(config); }; render() { diff --git a/scm-ui/src/config/modules/config.js b/scm-ui/src/config/modules/config.js index 5cf331c324..91b20a8a09 100644 --- a/scm-ui/src/config/modules/config.js +++ b/scm-ui/src/config/modules/config.js @@ -132,6 +132,7 @@ function removeNullValues(config: Config) { function reducer(state: any = {}, action: any = {}) { switch (action.type) { + case MODIFY_CONFIG_SUCCESS: case FETCH_CONFIG_SUCCESS: const config = removeNullValues(action.payload); return { From dd76a00aa1602643897410e474b75e1831ed09a3 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 10:10:59 +0200 Subject: [PATCH 51/53] remove outdated todo --- scm-ui/src/config/components/form/ProxySettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-ui/src/config/components/form/ProxySettings.js b/scm-ui/src/config/components/form/ProxySettings.js index 42cd0ab228..9b9976cbb4 100644 --- a/scm-ui/src/config/components/form/ProxySettings.js +++ b/scm-ui/src/config/components/form/ProxySettings.js @@ -12,7 +12,7 @@ type Props = { proxyServer: string, proxyUser: string, enableProxy: boolean, - proxyExcludes: string[], //TODO: einbauen! + proxyExcludes: string[], t: string => string, onChange: (boolean, any, string) => void, hasUpdatePermission: boolean From 5eb55a9baa7b39e8e8fecc9ef216e74181acd753 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 10:12:01 +0200 Subject: [PATCH 52/53] extract logic of AdminGroupTable, AdminUserTable and ProxyExcludesTable in to ArrayConfigTable --- .../components/table/AdminGroupTable.js | 42 ++++++---------- .../config/components/table/AdminUserTable.js | 46 ++++++------------ .../components/table/ArrayConfigTable.js | 48 +++++++++++++++++++ .../components/table/ProxyExcludesTable.js | 38 ++++----------- 4 files changed, 87 insertions(+), 87 deletions(-) create mode 100644 scm-ui/src/config/components/table/ArrayConfigTable.js diff --git a/scm-ui/src/config/components/table/AdminGroupTable.js b/scm-ui/src/config/components/table/AdminGroupTable.js index fc05c75f06..db9f83af84 100644 --- a/scm-ui/src/config/components/table/AdminGroupTable.js +++ b/scm-ui/src/config/components/table/AdminGroupTable.js @@ -1,48 +1,34 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import { RemoveEntryOfTableButton } from "../../../components/buttons"; +import ArrayConfigTable from "./ArrayConfigTable"; type Props = { adminGroups: string[], - t: string => string, onChange: (boolean, any, string) => void, - disabled: boolean + disabled: boolean, + + // context props + t: string => string }; type State = {}; class AdminGroupTable extends React.Component<Props, State> { render() { - const { t, disabled } = this.props; + const { t, disabled, adminGroups } = this.props; return ( - <div> - <label className="label">{t("admin-settings.admin-groups")}</label> - <table className="table is-hoverable is-fullwidth"> - <tbody> - {this.props.adminGroups.map(group => { - return ( - <tr key={group}> - <td key={group}>{group}</td> - <td> - <RemoveEntryOfTableButton - entryname={group} - removeEntry={this.removeEntry} - disabled={disabled} - label={t("admin-settings.remove-group-button")} - /> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> + <ArrayConfigTable + items={adminGroups} + label={t("admin-settings.admin-groups")} + removeLabel={t("admin-settings.remove-group-button")} + onRemove={this.removeEntry} + disabled={disabled} + /> ); } - removeEntry = (groupname: string) => { - const newGroups = this.props.adminGroups.filter(name => name !== groupname); + removeEntry = (newGroups: string[]) => { this.props.onChange(true, newGroups, "adminGroups"); }; } diff --git a/scm-ui/src/config/components/table/AdminUserTable.js b/scm-ui/src/config/components/table/AdminUserTable.js index c622a0e027..d1f35e8424 100644 --- a/scm-ui/src/config/components/table/AdminUserTable.js +++ b/scm-ui/src/config/components/table/AdminUserTable.js @@ -1,48 +1,32 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import { RemoveEntryOfTableButton } from "../../../components/buttons"; +import ArrayConfigTable from "./ArrayConfigTable"; type Props = { adminUsers: string[], - t: string => string, onChange: (boolean, any, string) => void, - disabled: boolean + disabled: boolean, + + // context props + t: string => string }; -type State = {}; - -class AdminUserTable extends React.Component<Props, State> { +class AdminUserTable extends React.Component<Props> { render() { - const { t, disabled } = this.props; + const { adminUsers, t, disabled } = this.props; return ( - <div> - <label className="label">{t("admin-settings.admin-users")}</label> - <table className="table is-hoverable is-fullwidth"> - <tbody> - {this.props.adminUsers.map(user => { - return ( - <tr key={user}> - <td key={user}>{user}</td> - <td> - <RemoveEntryOfTableButton - entryname={user} - removeEntry={this.removeEntry} - disabled={disabled} - label={t("admin-settings.remove-user-button")} - /> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> + <ArrayConfigTable + items={adminUsers} + label={t("admin-settings.admin-users")} + removeLabel={t("admin-settings.remove-user-button")} + onRemove={this.removeEntry} + disabled={disabled} + /> ); } - removeEntry = (username: string) => { - const newUsers = this.props.adminUsers.filter(name => name !== username); + removeEntry = (newUsers: string[]) => { this.props.onChange(true, newUsers, "adminUsers"); }; } diff --git a/scm-ui/src/config/components/table/ArrayConfigTable.js b/scm-ui/src/config/components/table/ArrayConfigTable.js new file mode 100644 index 0000000000..f47e6e9dca --- /dev/null +++ b/scm-ui/src/config/components/table/ArrayConfigTable.js @@ -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; diff --git a/scm-ui/src/config/components/table/ProxyExcludesTable.js b/scm-ui/src/config/components/table/ProxyExcludesTable.js index 4476442c48..a7849ffdf2 100644 --- a/scm-ui/src/config/components/table/ProxyExcludesTable.js +++ b/scm-ui/src/config/components/table/ProxyExcludesTable.js @@ -1,7 +1,7 @@ //@flow import React from "react"; import { translate } from "react-i18next"; -import { RemoveEntryOfTableButton } from "../../../components/buttons"; +import ArrayConfigTable from "./ArrayConfigTable"; type Props = { proxyExcludes: string[], @@ -14,37 +14,19 @@ type State = {}; class ProxyExcludesTable extends React.Component<Props, State> { render() { - const { t } = this.props; + const { proxyExcludes, disabled, t } = this.props; return ( - <div> - <label className="label">{t("proxy-settings.proxy-excludes")}</label> - <table className="table is-hoverable is-fullwidth"> - <tbody> - {this.props.proxyExcludes.map(excludes => { - return ( - <tr key={excludes}> - <td key={excludes}>{excludes}</td> - <td> - <RemoveEntryOfTableButton - entryname={excludes} - removeEntry={this.removeEntry} - disabled={this.props.disabled} - label={t("proxy-settings.remove-proxy-exclude-button")} - /> - </td> - </tr> - ); - })} - </tbody> - </table> - </div> + <ArrayConfigTable + items={proxyExcludes} + label={t("proxy-settings.proxy-excludes")} + removeLabel={t("proxy-settings.remove-proxy-exclude-button")} + onRemove={this.removeEntry} + disabled={disabled} + /> ); } - removeEntry = (excludename: string) => { - const newExcludes = this.props.proxyExcludes.filter( - name => name !== excludename - ); + removeEntry = (newExcludes: string[]) => { this.props.onChange(true, newExcludes, "proxyExcludes"); }; } From 7ccd26bc6ffed57c9c0486f8bcde0f13ef7f3c83 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra <sebastian.sdorra@cloudogu.com> Date: Thu, 30 Aug 2018 08:14:18 +0000 Subject: [PATCH 53/53] Close branch feature/ui-for-scm2_global-config