merge with 2.0.0-m3

This commit is contained in:
Sebastian Sdorra
2019-03-21 10:47:33 +01:00
91 changed files with 2102 additions and 1141 deletions

View File

@@ -2,7 +2,7 @@
import React from "react";
import { translate } from "react-i18next";
import { SubmitButton, Notification } from "@scm-manager/ui-components";
import type { Config } from "@scm-manager/ui-types";
import type { NamespaceStrategies, Config } from "@scm-manager/ui-types";
import ProxySettings from "./ProxySettings";
import GeneralSettings from "./GeneralSettings";
import BaseUrlSettings from "./BaseUrlSettings";
@@ -12,9 +12,11 @@ type Props = {
submitForm: Config => void,
config?: Config,
loading?: boolean,
t: string => string,
configReadPermission: boolean,
configUpdatePermission: boolean
configUpdatePermission: boolean,
namespaceStrategies?: NamespaceStrategies,
// context props
t: string => string,
};
type State = {
@@ -51,7 +53,7 @@ class ConfigForm extends React.Component<Props, State> {
pluginUrl: "",
loginAttemptLimitTimeout: 0,
enabledXsrfProtection: true,
defaultNamespaceStrategy: "",
namespaceStrategy: "",
_links: {}
},
showNotification: false,
@@ -85,6 +87,7 @@ class ConfigForm extends React.Component<Props, State> {
const {
loading,
t,
namespaceStrategies,
configReadPermission,
configUpdatePermission
} = this.props;
@@ -115,6 +118,7 @@ class ConfigForm extends React.Component<Props, State> {
<form onSubmit={this.submit}>
{noPermissionNotification}
<GeneralSettings
namespaceStrategies={namespaceStrategies}
realmDescription={config.realmDescription}
enableRepositoryArchive={config.enableRepositoryArchive}
disableGroupingGrid={config.disableGroupingGrid}
@@ -123,7 +127,7 @@ class ConfigForm extends React.Component<Props, State> {
skipFailedAuthenticators={config.skipFailedAuthenticators}
pluginUrl={config.pluginUrl}
enabledXsrfProtection={config.enabledXsrfProtection}
defaultNamespaceStrategy={config.defaultNamespaceStrategy}
namespaceStrategy={config.namespaceStrategy}
onChange={(isValid, changedValue, name) =>
this.onChange(isValid, changedValue, name)
}

View File

@@ -2,6 +2,8 @@
import React from "react";
import { translate } from "react-i18next";
import { Checkbox, InputField } from "@scm-manager/ui-components";
import type { NamespaceStrategies } from "@scm-manager/ui-types";
import NamespaceStrategySelect from "./NamespaceStrategySelect";
type Props = {
realmDescription: string,
@@ -12,10 +14,12 @@ type Props = {
skipFailedAuthenticators: boolean,
pluginUrl: string,
enabledXsrfProtection: boolean,
defaultNamespaceStrategy: string,
t: string => string,
namespaceStrategy: string,
namespaceStrategies?: NamespaceStrategies,
onChange: (boolean, any, string) => void,
hasUpdatePermission: boolean
hasUpdatePermission: boolean,
// context props
t: string => string
};
class GeneralSettings extends React.Component<Props> {
@@ -23,142 +27,58 @@ class GeneralSettings extends React.Component<Props> {
const {
t,
realmDescription,
enableRepositoryArchive,
disableGroupingGrid,
dateFormat,
anonymousAccessEnabled,
skipFailedAuthenticators,
pluginUrl,
enabledXsrfProtection,
defaultNamespaceStrategy,
hasUpdatePermission
namespaceStrategy,
hasUpdatePermission,
namespaceStrategies
} = this.props;
return (
<div>
<div className="columns">
<div className="column is-half">
<InputField
label={t("general-settings.realm-description")}
onChange={this.handleRealmDescriptionChange}
value={realmDescription}
disabled={!hasUpdatePermission}
helpText={t("help.realmDescriptionHelpText")}
/>
</div>
<div className="column is-half">
<InputField
label={t("general-settings.date-format")}
onChange={this.handleDateFormatChange}
value={dateFormat}
disabled={!hasUpdatePermission}
helpText={t("help.dateFormatHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column is-half">
<InputField
label={t("general-settings.plugin-url")}
onChange={this.handlePluginUrlChange}
value={pluginUrl}
disabled={!hasUpdatePermission}
helpText={t("help.pluginRepositoryHelpText")}
/>
</div>
<div className="column is-half">
<InputField
label={t("general-settings.default-namespace-strategy")}
onChange={this.handleDefaultNamespaceStrategyChange}
value={defaultNamespaceStrategy}
disabled={!hasUpdatePermission}
helpText={t("help.defaultNameSpaceStrategyHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column is-half">
<Checkbox
checked={enabledXsrfProtection}
label={t("general-settings.enabled-xsrf-protection")}
onChange={this.handleEnabledXsrfProtectionChange}
disabled={!hasUpdatePermission}
helpText={t("help.enableXsrfProtectionHelpText")}
/>
</div>
<div className="column is-half">
<Checkbox
checked={enableRepositoryArchive}
label={t("general-settings.enable-repository-archive")}
onChange={this.handleEnableRepositoryArchiveChange}
disabled={!hasUpdatePermission}
helpText={t("help.enableRepositoryArchiveHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column is-half">
<Checkbox
checked={disableGroupingGrid}
label={t("general-settings.disable-grouping-grid")}
onChange={this.handleDisableGroupingGridChange}
disabled={!hasUpdatePermission}
helpText={t("help.disableGroupingGridHelpText")}
/>
</div>
<div className="column is-half">
<Checkbox
checked={anonymousAccessEnabled}
label={t("general-settings.anonymous-access-enabled")}
onChange={this.handleAnonymousAccessEnabledChange}
disabled={!hasUpdatePermission}
helpText={t("help.allowAnonymousAccessHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column is-half">
<Checkbox
checked={skipFailedAuthenticators}
label={t("general-settings.skip-failed-authenticators")}
onChange={this.handleSkipFailedAuthenticatorsChange}
disabled={!hasUpdatePermission}
helpText={t("help.skipFailedAuthenticatorsHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column is-half">
<InputField
label={t("general-settings.realm-description")}
onChange={this.handleRealmDescriptionChange}
value={realmDescription}
disabled={!hasUpdatePermission}
helpText={t("help.realmDescriptionHelpText")}
/>
</div>
<div className="column is-half">
<NamespaceStrategySelect
label={t("general-settings.namespace-strategy")}
onChange={this.handleNamespaceStrategyChange}
value={namespaceStrategy}
disabled={!hasUpdatePermission}
namespaceStrategies={namespaceStrategies}
helpText={t("help.nameSpaceStrategyHelpText")}
/>
</div>
</div>
<div className="columns">
<div className="column is-half">
<Checkbox
checked={enabledXsrfProtection}
label={t("general-settings.enabled-xsrf-protection")}
onChange={this.handleEnabledXsrfProtectionChange}
disabled={!hasUpdatePermission}
helpText={t("help.enableXsrfProtectionHelpText")}
/>
</div>
</div>
</div>
);
}
handleRealmDescriptionChange = (value: string) => {
this.props.onChange(true, value, "realmDescription");
};
handleEnableRepositoryArchiveChange = (value: boolean) => {
this.props.onChange(true, value, "enableRepositoryArchive");
};
handleDisableGroupingGridChange = (value: boolean) => {
this.props.onChange(true, value, "disableGroupingGrid");
};
handleDateFormatChange = (value: string) => {
this.props.onChange(true, value, "dateFormat");
};
handleAnonymousAccessEnabledChange = (value: string) => {
this.props.onChange(true, value, "anonymousAccessEnabled");
};
handleSkipFailedAuthenticatorsChange = (value: string) => {
this.props.onChange(true, value, "skipFailedAuthenticators");
};
handlePluginUrlChange = (value: string) => {
this.props.onChange(true, value, "pluginUrl");
};
handleEnabledXsrfProtectionChange = (value: boolean) => {
this.props.onChange(true, value, "enabledXsrfProtection");
};
handleDefaultNamespaceStrategyChange = (value: string) => {
this.props.onChange(true, value, "defaultNamespaceStrategy");
handleNamespaceStrategyChange = (value: string) => {
this.props.onChange(true, value, "namespaceStrategy");
};
}

View File

@@ -0,0 +1,67 @@
//@flow
import React from "react";
import { translate, type TFunction } from "react-i18next";
import { Select } from "@scm-manager/ui-components";
import type { NamespaceStrategies } from "@scm-manager/ui-types";
type Props = {
namespaceStrategies: NamespaceStrategies,
label: string,
value?: string,
disabled?: boolean,
helpText?: string,
onChange: (value: string, name?: string) => void,
// context props
t: TFunction
};
class NamespaceStrategySelect extends React.Component<Props> {
createNamespaceOptions = () => {
const { namespaceStrategies, t } = this.props;
let available = [];
if (namespaceStrategies && namespaceStrategies.available) {
available = namespaceStrategies.available;
}
return available.map(ns => {
const key = "namespaceStrategies." + ns;
let label = t(key);
if (label === key) {
label = ns;
}
return {
value: ns,
label: label
};
});
};
findSelected = () => {
const { namespaceStrategies, value } = this.props;
if (
!namespaceStrategies ||
!namespaceStrategies.available ||
namespaceStrategies.available.indexOf(value) < 0
) {
return namespaceStrategies.current;
}
return value;
};
render() {
const { label, helpText, disabled, onChange } = this.props;
const nsOptions = this.createNamespaceOptions();
return (
<Select
label={label}
onChange={onChange}
value={this.findSelected()}
disabled={disabled}
options={nsOptions}
helpText={helpText}
/>
);
}
}
export default translate("plugins")(NamespaceStrategySelect);

View File

@@ -14,9 +14,15 @@ import {
modifyConfigReset
} from "../modules/config";
import { connect } from "react-redux";
import type { Config } from "@scm-manager/ui-types";
import type { Config, NamespaceStrategies } from "@scm-manager/ui-types";
import ConfigForm from "../components/form/ConfigForm";
import { getConfigLink } from "../../modules/indexResource";
import {
fetchNamespaceStrategiesIfNeeded,
getFetchNamespaceStrategiesFailure,
getNamespaceStrategies,
isFetchNamespaceStrategiesPending
} from "../modules/namespaceStrategies";
type Props = {
loading: boolean,
@@ -24,11 +30,13 @@ type Props = {
config: Config,
configUpdatePermission: boolean,
configLink: string,
namespaceStrategies?: NamespaceStrategies,
// dispatch functions
modifyConfig: (config: Config, callback?: () => void) => void,
fetchConfig: (link: string) => void,
configReset: void => void,
fetchNamespaceStrategiesIfNeeded: void => void,
// context objects
t: string => string
@@ -51,6 +59,7 @@ class GlobalConfig extends React.Component<Props, State> {
componentDidMount() {
this.props.configReset();
this.props.fetchNamespaceStrategiesIfNeeded();
if (this.props.configLink) {
this.props.fetchConfig(this.props.configLink);
} else {
@@ -103,7 +112,7 @@ class GlobalConfig extends React.Component<Props, State> {
};
renderContent = () => {
const { error, loading, config, configUpdatePermission } = this.props;
const { error, loading, config, configUpdatePermission, namespaceStrategies } = this.props;
const { configReadPermission } = this.state;
if (!error) {
return (
@@ -113,6 +122,7 @@ class GlobalConfig extends React.Component<Props, State> {
submitForm={config => this.modifyConfig(config)}
config={config}
loading={loading}
namespaceStrategies={namespaceStrategies}
configUpdatePermission={configUpdatePermission}
configReadPermission={configReadPermission}
/>
@@ -133,23 +143,33 @@ const mapDispatchToProps = dispatch => {
},
configReset: () => {
dispatch(modifyConfigReset());
},
fetchNamespaceStrategiesIfNeeded: () => {
dispatch(fetchNamespaceStrategiesIfNeeded());
}
};
};
const mapStateToProps = state => {
const loading = isFetchConfigPending(state) || isModifyConfigPending(state);
const error = getFetchConfigFailure(state) || getModifyConfigFailure(state);
const loading = isFetchConfigPending(state)
|| isModifyConfigPending(state)
|| isFetchNamespaceStrategiesPending(state);
const error = getFetchConfigFailure(state)
|| getModifyConfigFailure(state)
|| getFetchNamespaceStrategiesFailure(state);
const config = getConfig(state);
const configUpdatePermission = getConfigUpdatePermission(state);
const configLink = getConfigLink(state);
const namespaceStrategies = getNamespaceStrategies(state);
return {
loading,
error,
config,
configUpdatePermission,
configLink
configLink,
namespaceStrategies
};
};

View File

@@ -50,7 +50,7 @@ const config = {
"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",
namespaceStrategy: "UsernameNamespaceStrategy",
_links: {
self: { href: "http://localhost:8081/api/v2/config" },
update: { href: "http://localhost:8081/api/v2/config" }
@@ -79,7 +79,7 @@ const configWithNullValues = {
"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",
namespaceStrategy: "UsernameNamespaceStrategy",
_links: {
self: { href: "http://localhost:8081/api/v2/config" },
update: { href: "http://localhost:8081/api/v2/config" }

View File

@@ -0,0 +1,115 @@
// @flow
import * as types from "../../modules/types";
import type { Action, NamespaceStrategies } from "@scm-manager/ui-types";
import { apiClient } from "@scm-manager/ui-components";
import { isPending } from "../../modules/pending";
import { getFailure } from "../../modules/failure";
import { MODIFY_CONFIG_SUCCESS } from "./config";
export const FETCH_NAMESPACESTRATEGIES_TYPES =
"scm/config/FETCH_NAMESPACESTRATEGIES_TYPES";
export const FETCH_NAMESPACESTRATEGIES_TYPES_PENDING = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
types.PENDING_SUFFIX
}`;
export const FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
types.SUCCESS_SUFFIX
}`;
export const FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE = `${FETCH_NAMESPACESTRATEGIES_TYPES}_${
types.FAILURE_SUFFIX
}`;
export function fetchNamespaceStrategiesIfNeeded() {
return function(dispatch: any, getState: () => Object) {
const state = getState();
if (shouldFetchNamespaceStrategies(state)) {
return fetchNamespaceStrategies(
dispatch,
state.indexResources.links.namespaceStrategies.href
);
}
};
}
function fetchNamespaceStrategies(dispatch: any, url: string) {
dispatch(fetchNamespaceStrategiesPending());
return apiClient
.get(url)
.then(response => response.json())
.then(namespaceStrategies => {
dispatch(fetchNamespaceStrategiesSuccess(namespaceStrategies));
})
.catch(error => {
dispatch(fetchNamespaceStrategiesFailure(error));
});
}
export function shouldFetchNamespaceStrategies(state: Object) {
if (
isFetchNamespaceStrategiesPending(state) ||
getFetchNamespaceStrategiesFailure(state)
) {
return false;
}
return !state.namespaceStrategies || !state.namespaceStrategies.current;
}
export function fetchNamespaceStrategiesPending(): Action {
return {
type: FETCH_NAMESPACESTRATEGIES_TYPES_PENDING
};
}
export function fetchNamespaceStrategiesSuccess(
namespaceStrategies: NamespaceStrategies
): Action {
return {
type: FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
payload: namespaceStrategies
};
}
export function fetchNamespaceStrategiesFailure(error: Error): Action {
return {
type: FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE,
payload: error
};
}
// reducers
export default function reducer(
state: Object = {},
action: Action = { type: "UNKNOWN" }
): Object {
if (
action.type === FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS &&
action.payload
) {
return action.payload;
} else if (action.type === MODIFY_CONFIG_SUCCESS && action.payload) {
const config = action.payload;
return {
...state,
current: config.namespaceStrategy
};
}
return state;
}
// selectors
export function getNamespaceStrategies(state: Object) {
if (state.namespaceStrategies) {
return state.namespaceStrategies;
}
return {};
}
export function isFetchNamespaceStrategiesPending(state: Object) {
return isPending(state, FETCH_NAMESPACESTRATEGIES_TYPES);
}
export function getFetchNamespaceStrategiesFailure(state: Object) {
return getFailure(state, FETCH_NAMESPACESTRATEGIES_TYPES);
}

View File

@@ -0,0 +1,199 @@
// @flow
import fetchMock from "fetch-mock";
import configureMockStore from "redux-mock-store";
import thunk from "redux-thunk";
import {
FETCH_NAMESPACESTRATEGIES_TYPES,
FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE,
FETCH_NAMESPACESTRATEGIES_TYPES_PENDING,
FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
fetchNamespaceStrategiesIfNeeded,
fetchNamespaceStrategiesSuccess,
shouldFetchNamespaceStrategies,
default as reducer,
getNamespaceStrategies,
isFetchNamespaceStrategiesPending,
getFetchNamespaceStrategiesFailure
} from "./namespaceStrategies";
import { MODIFY_CONFIG_SUCCESS } from "./config";
const strategies = {
current: "UsernameNamespaceStrategy",
available: [
"UsernameNamespaceStrategy",
"CustomNamespaceStrategy",
"CurrentYearNamespaceStrategy",
"RepositoryTypeNamespaceStrategy"
],
_links: {
self: {
href: "http://localhost:8081/scm/api/v2/namespaceStrategies"
}
}
};
describe("namespace strategy caching", () => {
it("should fetch strategies, on empty state", () => {
expect(shouldFetchNamespaceStrategies({})).toBe(true);
});
it("should fetch strategies, on empty namespaceStrategies node", () => {
const state = {
namespaceStrategies: {}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(true);
});
it("should not fetch strategies, on pending state", () => {
const state = {
pending: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: true
}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
});
it("should not fetch strategies, on failure state", () => {
const state = {
failure: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: new Error("no...")
}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
});
it("should not fetch strategies, if they are already fetched", () => {
const state = {
namespaceStrategies: {
current: "some"
}
};
expect(shouldFetchNamespaceStrategies(state)).toBe(false);
});
});
describe("namespace strategies fetch", () => {
const URL = "http://scm.hitchhiker.com/api/v2/namespaceStrategies";
const mockStore = configureMockStore([thunk]);
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
const createStore = (initialState = {}) => {
return mockStore({
...initialState,
indexResources: {
links: {
namespaceStrategies: {
href: URL
}
}
}
});
};
it("should successfully fetch strategies", () => {
fetchMock.getOnce(URL, strategies);
const expectedActions = [
{ type: FETCH_NAMESPACESTRATEGIES_TYPES_PENDING },
{
type: FETCH_NAMESPACESTRATEGIES_TYPES_SUCCESS,
payload: strategies
}
];
const store = createStore();
return store.dispatch(fetchNamespaceStrategiesIfNeeded()).then(() => {
expect(store.getActions()).toEqual(expectedActions);
});
});
it("should dispatch FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE on server error", () => {
fetchMock.getOnce(URL, {
status: 500
});
const store = createStore();
return store.dispatch(fetchNamespaceStrategiesIfNeeded()).then(() => {
const actions = store.getActions();
expect(actions[0].type).toBe(FETCH_NAMESPACESTRATEGIES_TYPES_PENDING);
expect(actions[1].type).toBe(FETCH_NAMESPACESTRATEGIES_TYPES_FAILURE);
expect(actions[1].payload).toBeDefined();
});
});
it("should not dispatch any action, if the strategies are already fetched", () => {
const store = createStore({
namespaceStrategies: strategies
});
store.dispatch(fetchNamespaceStrategiesIfNeeded());
expect(store.getActions().length).toBe(0);
});
});
describe("namespace strategies reducer", () => {
it("should return unmodified state on unknown action", () => {
const state = {};
expect(reducer(state)).toBe(state);
});
it("should store the strategies on success", () => {
const newState = reducer({}, fetchNamespaceStrategiesSuccess(strategies));
expect(newState).toBe(strategies);
});
it("should clear store if config was modified", () => {
const modifyConfigAction = {
type: MODIFY_CONFIG_SUCCESS,
payload: {
namespaceStrategy: "CustomNamespaceStrategy"
}
};
const newState = reducer(strategies, modifyConfigAction);
expect(newState.current).toEqual("CustomNamespaceStrategy");
});
});
describe("namespace strategy selectors", () => {
const error = new Error("The end of the universe");
it("should return an empty object", () => {
expect(getNamespaceStrategies({})).toEqual({});
});
it("should return the namespace strategies", () => {
const state = {
namespaceStrategies: strategies
};
expect(getNamespaceStrategies(state)).toBe(strategies);
});
it("should return true, when fetch namespace strategies is pending", () => {
const state = {
pending: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: true
}
};
expect(isFetchNamespaceStrategiesPending(state)).toEqual(true);
});
it("should return false, when fetch strategies is not pending", () => {
expect(isFetchNamespaceStrategiesPending({})).toEqual(false);
});
it("should return error when fetch namespace strategies did fail", () => {
const state = {
failure: {
[FETCH_NAMESPACESTRATEGIES_TYPES]: error
}
};
expect(getFetchNamespaceStrategiesFailure(state)).toEqual(error);
});
it("should return undefined when fetch strategies did not fail", () => {
expect(getFetchNamespaceStrategiesFailure({})).toBe(undefined);
});
});

View File

@@ -5,7 +5,7 @@ import { connect } from "react-redux";
import { translate } from "react-i18next";
import { withRouter } from "react-router-dom";
import { Loading, ErrorPage } from "@scm-manager/ui-components";
import { Loading, ErrorBoundary } from "@scm-manager/ui-components";
import {
fetchIndexResources,
getFetchIndexResourcesFailure,
@@ -15,6 +15,7 @@ import {
import PluginLoader from "./PluginLoader";
import type { IndexResources } from "@scm-manager/ui-types";
import ScrollToTop from "./ScrollToTop";
import IndexErrorPage from "./IndexErrorPage";
type Props = {
error: Error,
@@ -55,25 +56,21 @@ class Index extends Component<Props, State> {
const { pluginsLoaded } = this.state;
if (error) {
return (
<ErrorPage
title={t("app.error.title")}
subtitle={t("app.error.subtitle")}
error={error}
/>
);
return <IndexErrorPage error={error}/>;
} else if (loading || !indexResources) {
return <Loading />;
} else {
return (
<ScrollToTop>
<PluginLoader
loaded={pluginsLoaded}
callback={this.pluginLoaderCallback}
>
<App />
</PluginLoader>
</ScrollToTop>
<ErrorBoundary fallback={IndexErrorPage}>
<ScrollToTop>
<PluginLoader
loaded={pluginsLoaded}
callback={this.pluginLoaderCallback}
>
<App />
</PluginLoader>
</ScrollToTop>
</ErrorBoundary>
);
}
}

View File

@@ -0,0 +1,26 @@
//@flow
import React from "react";
import { translate, type TFunction } from "react-i18next";
import { ErrorPage } from "@scm-manager/ui-components";
type Props = {
error: Error,
t: TFunction
}
class IndexErrorPage extends React.Component<Props> {
render() {
const { error, t } = this.props;
return (
<ErrorPage
title={t("app.error.title")}
subtitle={t("app.error.subtitle")}
error={error}
/>
);
}
}
export default translate("commons")(IndexErrorPage);

View File

@@ -9,7 +9,7 @@ import Users from "../users/containers/Users";
import Login from "../containers/Login";
import Logout from "../containers/Logout";
import { ProtectedRoute } from "@scm-manager/ui-components";
import {ProtectedRoute} from "@scm-manager/ui-components";
import {binder, ExtensionPoint } from "@scm-manager/ui-extensions";
import AddUser from "../users/containers/AddUser";

View File

@@ -81,7 +81,14 @@ class PluginLoader extends React.Component<Props, State> {
};
loadBundle = (bundle: string) => {
return fetch(bundle)
return fetch(bundle, {
credentials: "same-origin",
headers: {
Cache: "no-cache",
// identify the request as ajax request
"X-Requested-With": "XMLHttpRequest"
}
})
.then(response => {
return response.text();
})

View File

@@ -15,6 +15,7 @@ import pending from "./modules/pending";
import failure from "./modules/failure";
import permissions from "./repos/permissions/modules/permissions";
import config from "./config/modules/config";
import namespaceStrategies from "./config/modules/namespaceStrategies";
import indexResources from "./modules/indexResource";
import type { BrowserHistory } from "history/createBrowserHistory";
@@ -38,7 +39,8 @@ function createReduxStore(history: BrowserHistory) {
groups,
auth,
config,
sources
sources,
namespaceStrategies
});
return createStore(

View File

@@ -1,30 +1,30 @@
//@flow
import React from "react";
import type { Repository } from "@scm-manager/ui-types";
import RepositoryDetailTable from "./RepositoryDetailTable";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
type Props = {
repository: Repository
};
class RepositoryDetails extends React.Component<Props> {
render() {
const { repository } = this.props;
return (
<div>
<RepositoryDetailTable repository={repository} />
<hr />
<div className="content">
<ExtensionPoint
name="repos.repository-details.information"
renderAll={true}
props={{ repository }}
/>
</div>
</div>
);
}
}
export default RepositoryDetails;
//@flow
import React from "react";
import type { Repository } from "@scm-manager/ui-types";
import RepositoryDetailTable from "./RepositoryDetailTable";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
type Props = {
repository: Repository
};
class RepositoryDetails extends React.Component<Props> {
render() {
const { repository } = this.props;
return (
<div>
<RepositoryDetailTable repository={repository}/>
<hr/>
<div className="content">
<ExtensionPoint
name="repos.repository-details.information"
renderAll={true}
props={{ repository }}
/>
</div>
</div>
);
}
}
export default RepositoryDetails;

View File

@@ -8,6 +8,7 @@ import {
SubmitButton,
Textarea
} from "@scm-manager/ui-components";
import { ExtensionPoint } from "@scm-manager/ui-extensions";
import type { Repository, RepositoryType } from "@scm-manager/ui-types";
import * as validator from "./repositoryValidation";
@@ -15,16 +16,20 @@ type Props = {
submitForm: Repository => void,
repository?: Repository,
repositoryTypes: RepositoryType[],
namespaceStrategy: string,
loading?: boolean,
t: string => string
};
type State = {
repository: Repository,
namespaceValidationError: boolean,
nameValidationError: boolean,
contactValidationError: boolean
};
const CUSTOM_NAMESPACE_STRATEGY = "CustomNamespaceStrategy";
class RepositoryForm extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
@@ -38,9 +43,9 @@ class RepositoryForm extends React.Component<Props, State> {
description: "",
_links: {}
},
namespaceValidationError: false,
nameValidationError: false,
contactValidationError: false,
descriptionValidationError: false
contactValidationError: false
};
}
@@ -59,11 +64,15 @@ class RepositoryForm extends React.Component<Props, State> {
}
isValid = () => {
const repository = this.state.repository;
const { namespaceStrategy } = this.props;
const { repository } = this.state;
return !(
this.state.namespaceValidationError ||
this.state.nameValidationError ||
this.state.contactValidationError ||
this.isFalsy(repository.name)
this.isFalsy(repository.name) ||
(namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY &&
this.isFalsy(repository.namespace))
);
};
@@ -78,10 +87,24 @@ class RepositoryForm extends React.Component<Props, State> {
return !this.props.repository;
};
isModifiable = () => {
return !!this.props.repository && !!this.props.repository._links.update;
};
render() {
const { loading, t } = this.props;
const repository = this.state.repository;
const disabled = !this.isModifiable() && !this.isCreateMode();
const submitButton = disabled ? null : (
<SubmitButton
disabled={!this.isValid()}
loading={loading}
label={t("repositoryForm.submit")}
/>
);
let subtitle = null;
if (this.props.repository) {
// edit existing repo
@@ -100,6 +123,7 @@ class RepositoryForm extends React.Component<Props, State> {
validationError={this.state.contactValidationError}
errorMessage={t("validation.contact-invalid")}
helpText={t("help.contactHelpText")}
disabled={disabled}
/>
<Textarea
@@ -107,12 +131,9 @@ class RepositoryForm extends React.Component<Props, State> {
onChange={this.handleDescriptionChange}
value={repository ? repository.description : ""}
helpText={t("help.descriptionHelpText")}
disabled={disabled}
/>
<SubmitButton
disabled={!this.isValid()}
loading={loading}
label={t("repositoryForm.submit")}
/>
{submitButton}
</form>
</>
);
@@ -127,6 +148,31 @@ class RepositoryForm extends React.Component<Props, State> {
});
}
renderNamespaceField = () => {
const { namespaceStrategy, t } = this.props;
const repository = this.state.repository;
const props = {
label: t("repository.namespace"),
helpText: t("help.namespaceHelpText"),
value: repository ? repository.namespace : "",
onChange: this.handleNamespaceChange,
errorMessage: t("validation.namespace-invalid"),
validationError: this.state.namespaceValidationError
};
if (namespaceStrategy === CUSTOM_NAMESPACE_STRATEGY) {
return <InputField {...props} />;
}
return (
<ExtensionPoint
name="repos.create.namespace"
props={props}
renderAll={false}
/>
);
};
renderCreateOnlyFields() {
if (!this.isCreateMode()) {
return null;
@@ -135,6 +181,7 @@ class RepositoryForm extends React.Component<Props, State> {
const repository = this.state.repository;
return (
<>
{this.renderNamespaceField()}
<InputField
label={t("repository.name")}
onChange={this.handleNameChange}
@@ -154,6 +201,13 @@ class RepositoryForm extends React.Component<Props, State> {
);
}
handleNamespaceChange = (namespace: string) => {
this.setState({
namespaceValidationError: !validator.isNameValid(namespace),
repository: { ...this.state.repository, namespace }
});
};
handleNameChange = (name: string) => {
this.setState({
nameValidationError: !validator.isNameValid(name),

View File

@@ -1,8 +1,10 @@
// @flow
import { validation } from "@scm-manager/ui-components";
const nameRegex = /(?!^\.\.$)(?!^\.$)(?!.*[\\\[\]])^[A-Za-z0-9\.][A-Za-z0-9\.\-_]*$/;
export const isNameValid = (name: string) => {
return validation.isNameValid(name);
return nameRegex.test(name);
};
export function isContactValid(mail: string) {

View File

@@ -11,6 +11,81 @@ describe("repository name validation", () => {
expect(validator.isNameValid("scm/manager")).toBe(false);
expect(validator.isNameValid("scm/ma/nager")).toBe(false);
});
it("should allow same names as the backend", () => {
const validPaths = [
"scm",
"s",
"sc",
".hiddenrepo",
"b.",
"...",
"..c",
"d..",
"a..c"
];
validPaths.forEach((path) =>
expect(validator.isNameValid(path)).toBe(true)
);
});
it("should deny same names as the backend", () => {
const invalidPaths = [
".",
"/",
"//",
"..",
"/.",
"/..",
"./",
"../",
"/../",
"/./",
"/...",
"/abc",
".../",
"/sdf/",
"asdf/",
"./b",
"scm/plugins/.",
"scm/../plugins",
"scm/main/",
"/scm/main/",
"scm/./main",
"scm//main",
"scm\\main",
"scm/main-$HOME",
"scm/main-${HOME}-home",
"scm/main-%HOME-home",
"scm/main-%HOME%-home",
"abc$abc",
"abc%abc",
"abc<abc",
"abc>abc",
"abc#abc",
"abc+abc",
"abc{abc",
"abc}abc",
"abc(abc",
"abc)abc",
"abc[abc",
"abc]abc",
"abc|abc",
"scm/main",
"scm/plugins/git-plugin",
".scm/plugins",
"a/b..",
"a/..b",
"scm/main",
"scm/plugins/git-plugin",
"scm/plugins/git-plugin"
];
invalidPaths.forEach((path) =>
expect(validator.isNameValid(path)).toBe(false)
);
});
});
describe("repository contact validation", () => {

View File

@@ -4,7 +4,7 @@ import { connect } from "react-redux";
import { translate } from "react-i18next";
import { Page } from "@scm-manager/ui-components";
import RepositoryForm from "../components/form";
import type { Repository, RepositoryType } from "@scm-manager/ui-types";
import type { Repository, RepositoryType, NamespaceStrategies } from "@scm-manager/ui-types";
import {
fetchRepositoryTypesIfNeeded,
getFetchRepositoryTypesFailure,
@@ -19,15 +19,21 @@ import {
} from "../modules/repos";
import type { History } from "history";
import { getRepositoriesLink } from "../../modules/indexResource";
import {
fetchNamespaceStrategiesIfNeeded,
getFetchNamespaceStrategiesFailure, getNamespaceStrategies, isFetchNamespaceStrategiesPending
} from "../../config/modules/namespaceStrategies";
type Props = {
repositoryTypes: RepositoryType[],
typesLoading: boolean,
namespaceStrategies: NamespaceStrategies,
pageLoading: boolean,
createLoading: boolean,
error: Error,
repoLink: string,
// dispatch functions
fetchNamespaceStrategiesIfNeeded: () => void,
fetchRepositoryTypesIfNeeded: () => void,
createRepo: (
link: string,
@@ -45,6 +51,7 @@ class Create extends React.Component<Props> {
componentDidMount() {
this.props.resetForm();
this.props.fetchRepositoryTypesIfNeeded();
this.props.fetchNamespaceStrategiesIfNeeded();
}
repoCreated = (repo: Repository) => {
@@ -55,9 +62,10 @@ class Create extends React.Component<Props> {
render() {
const {
typesLoading,
pageLoading,
createLoading,
repositoryTypes,
namespaceStrategies,
createRepo,
error
} = this.props;
@@ -67,13 +75,14 @@ class Create extends React.Component<Props> {
<Page
title={t("create.title")}
subtitle={t("create.subtitle")}
loading={typesLoading}
loading={pageLoading}
error={error}
showContentOnError={true}
>
<RepositoryForm
repositoryTypes={repositoryTypes}
loading={createLoading}
namespaceStrategy={namespaceStrategies.current}
submitForm={repo => {
createRepo(repoLink, repo, (repo: Repository) =>
this.repoCreated(repo)
@@ -87,14 +96,18 @@ class Create extends React.Component<Props> {
const mapStateToProps = state => {
const repositoryTypes = getRepositoryTypes(state);
const typesLoading = isFetchRepositoryTypesPending(state);
const namespaceStrategies = getNamespaceStrategies(state);
const pageLoading = isFetchRepositoryTypesPending(state)
|| isFetchNamespaceStrategiesPending(state);
const createLoading = isCreateRepoPending(state);
const error =
getFetchRepositoryTypesFailure(state) || getCreateRepoFailure(state);
const error = getFetchRepositoryTypesFailure(state)
|| getCreateRepoFailure(state)
|| getFetchNamespaceStrategiesFailure(state);
const repoLink = getRepositoriesLink(state);
return {
repositoryTypes,
typesLoading,
namespaceStrategies,
pageLoading,
createLoading,
error,
repoLink
@@ -106,6 +119,9 @@ const mapDispatchToProps = dispatch => {
fetchRepositoryTypesIfNeeded: () => {
dispatch(fetchRepositoryTypesIfNeeded());
},
fetchNamespaceStrategiesIfNeeded: () => {
dispatch(fetchNamespaceStrategiesIfNeeded());
},
createRepo: (
link: string,
repository: Repository,

View File

@@ -1,11 +1,9 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { apiClient } from "@scm-manager/ui-components";
import { apiClient, SyntaxHighlighter } from "@scm-manager/ui-components";
import type { File } from "@scm-manager/ui-types";
import { ErrorNotification, Loading } from "@scm-manager/ui-components";
import SyntaxHighlighter from "react-syntax-highlighter";
import { arduinoLight } from "react-syntax-highlighter/styles/hljs";
type Props = {
t: string => string,
@@ -68,12 +66,9 @@ class SourcecodeViewer extends React.Component<Props, State> {
return (
<SyntaxHighlighter
showLineNumbers="true"
language={getLanguage(language)}
style={arduinoLight}
>
{content}
</SyntaxHighlighter>
value= {content}
/>
);
}
}