Merged in feature/ui-error-handling (pull request #203)

Feature/ui error handling
This commit is contained in:
René Pfeuffer
2019-03-04 16:12:30 +00:00
26 changed files with 555 additions and 161 deletions

View File

@@ -0,0 +1,123 @@
// @flow
import React from "react";
import { BackendError } from "./errors";
import Notification from "./Notification";
import { translate } from "react-i18next";
type Props = { error: BackendError, t: string => string };
class BackendErrorNotification extends React.Component<Props> {
constructor(props: Props) {
super(props);
}
render() {
return (
<Notification type="danger">
<div className="content">
<p className="subtitle">{this.renderErrorName()}</p>
<p>{this.renderErrorDescription()}</p>
<p>{this.renderViolations()}</p>
{this.renderMetadata()}
</div>
</Notification>
);
}
renderErrorName = () => {
const { error, t } = this.props;
const translation = t("errors." + error.errorCode + ".displayName");
if (translation === error.errorCode) {
return error.message;
}
return translation;
};
renderErrorDescription = () => {
const { error, t } = this.props;
const translation = t("errors." + error.errorCode + ".description");
if (translation === error.errorCode) {
return "";
}
return translation;
};
renderViolations = () => {
const { error, t } = this.props;
if (error.violations) {
return (
<>
<p>
<strong>{t("errors.violations")}</strong>
</p>
<ul>
{error.violations.map((violation, index) => {
return (
<li key={index}>
<strong>{violation.path}:</strong> {violation.message}
</li>
);
})}
</ul>
</>
);
}
};
renderMetadata = () => {
const { error, t } = this.props;
return (
<>
{this.renderContext()}
{this.renderMoreInformationLink()}
<div className="level is-size-7">
<div className="left">
{t("errors.transactionId")} {error.transactionId}
</div>
<div className="right">
{t("errors.errorCode")} {error.errorCode}
</div>
</div>
</>
);
};
renderContext = () => {
const { error, t} = this.props;
if (error.context) {
return (
<>
<p>
<strong>{t("errors.context")}</strong>
</p>
<ul>
{error.context.map((context, index) => {
return (
<li key={index}>
<strong>{context.type}:</strong> {context.id}
</li>
);
})}
</ul>
</>
);
}
};
renderMoreInformationLink = () => {
const { error, t } = this.props;
if (error.url) {
return (
<p>
{t("errors.moreInfo")}{" "}
<a href={error.url} target="_blank">
{error.errorCode}
</a>
</p>
);
}
};
}
export default translate("plugins")(BackendErrorNotification);

View File

@@ -1,28 +1,41 @@
//@flow
import React from "react";
import { translate } from "react-i18next";
import { BackendError, ForbiddenError, UnauthorizedError } from "./errors";
import Notification from "./Notification";
import {UNAUTHORIZED_ERROR} from "./apiclient";
import BackendErrorNotification from "./BackendErrorNotification";
type Props = {
t: string => string,
error?: Error
};
class ErrorNotification extends React.Component<Props> {
class ErrorNotification extends React.Component<Props> {
render() {
const { t, error } = this.props;
if (error) {
if (error === UNAUTHORIZED_ERROR) {
if (error instanceof BackendError) {
return <BackendErrorNotification error={error} />
} else if (error instanceof UnauthorizedError) {
return (
<Notification type="danger">
<strong>{t("error-notification.prefix")}:</strong> {t("error-notification.timeout")}
{" "}
<a href="javascript:window.location.reload(true)">{t("error-notification.loginLink")}</a>
<strong>{t("error-notification.prefix")}:</strong>{" "}
{t("error-notification.timeout")}{" "}
<a href="javascript:window.location.reload(true)">
{t("error-notification.loginLink")}
</a>
</Notification>
);
} else {
} else if (error instanceof ForbiddenError) {
return (
<Notification type="danger">
<strong>{t("error-notification.prefix")}:</strong>{" "}
{t("error-notification.forbidden")}
</Notification>
)
} else
{
return (
<Notification type="danger">
<strong>{t("error-notification.prefix")}:</strong> {error.message}

View File

@@ -1,6 +1,7 @@
//@flow
import React from "react";
import ErrorNotification from "./ErrorNotification";
import { BackendError, ForbiddenError } from "./errors";
type Props = {
error: Error,
@@ -10,18 +11,26 @@ type Props = {
class ErrorPage extends React.Component<Props> {
render() {
const { title, subtitle, error } = this.props;
const { title, error } = this.props;
return (
<section className="section">
<div className="box column is-4 is-offset-4 container">
<h1 className="title">{title}</h1>
<p className="subtitle">{subtitle}</p>
{this.renderSubtitle()}
<ErrorNotification error={error} />
</div>
</section>
);
}
renderSubtitle = () => {
const { error, subtitle } = this.props;
if (error instanceof BackendError || error instanceof ForbiddenError) {
return null;
}
return <p className="subtitle">{subtitle}</p>
}
}
export default ErrorPage;

View File

@@ -1,9 +1,7 @@
// @flow
import {contextPath} from "./urls";
export const NOT_FOUND_ERROR = new Error("not found");
export const UNAUTHORIZED_ERROR = new Error("unauthorized");
export const CONFLICT_ERROR = new Error("conflict");
import { contextPath } from "./urls";
import { createBackendError, ForbiddenError, isBackendError, UnauthorizedError } from "./errors";
import type { BackendErrorContent } from "./errors";
const fetchOptions: RequestOptions = {
credentials: "same-origin",
@@ -12,19 +10,24 @@ const fetchOptions: RequestOptions = {
}
};
function handleStatusCode(response: Response) {
function handleFailure(response: Response) {
if (!response.ok) {
switch (response.status) {
case 401:
throw UNAUTHORIZED_ERROR;
case 404:
throw NOT_FOUND_ERROR;
case 409:
throw CONFLICT_ERROR;
default:
throw new Error("server returned status code " + response.status);
if (isBackendError(response)) {
return response.json()
.then((content: BackendErrorContent) => {
throw createBackendError(content, response.status);
});
} else {
if (response.status === 401) {
throw new UnauthorizedError("Unauthorized", 401);
} else if (response.status === 403) {
throw new ForbiddenError("Forbidden", 403);
}
throw new Error("server returned status code " + response.status);
}
}
return response;
}
@@ -42,7 +45,7 @@ export function createUrl(url: string) {
class ApiClient {
get(url: string): Promise<Response> {
return fetch(createUrl(url), fetchOptions).then(handleStatusCode);
return fetch(createUrl(url), fetchOptions).then(handleFailure);
}
post(url: string, payload: any, contentType: string = "application/json") {
@@ -58,7 +61,7 @@ class ApiClient {
method: "HEAD"
};
options = Object.assign(options, fetchOptions);
return fetch(createUrl(url), options).then(handleStatusCode);
return fetch(createUrl(url), options).then(handleFailure);
}
delete(url: string): Promise<Response> {
@@ -66,7 +69,7 @@ class ApiClient {
method: "DELETE"
};
options = Object.assign(options, fetchOptions);
return fetch(createUrl(url), options).then(handleStatusCode);
return fetch(createUrl(url), options).then(handleFailure);
}
httpRequestWithJSONBody(
@@ -83,7 +86,7 @@ class ApiClient {
// $FlowFixMe
options.headers["Content-Type"] = contentType;
return fetch(createUrl(url), options).then(handleStatusCode);
return fetch(createUrl(url), options).then(handleFailure);
}
}

View File

@@ -1,14 +1,9 @@
// @flow
import {apiClient, createUrl} from "./apiclient";
import fetchMock from "fetch-mock";
import { apiClient, createUrl } from "./apiclient";
import {fetchMock} from "fetch-mock";
import { BackendError } from "./errors";
describe("apiClient", () => {
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
describe("create url", () => {
describe("create url", () => {
it("should not change absolute urls", () => {
expect(createUrl("https://www.scm-manager.org")).toBe(
"https://www.scm-manager.org"
@@ -19,47 +14,65 @@ describe("apiClient", () => {
expect(createUrl("/users")).toBe("/api/v2/users");
expect(createUrl("users")).toBe("/api/v2/users");
});
});
});
describe("error handling", () => {
const error = {
message: "Error!!"
describe("error handling tests", () => {
const earthNotFoundError = {
transactionId: "42t",
errorCode: "42e",
message: "earth not found",
context: [{
type: "planet",
id: "earth"
}]
};
it("should append default error message for 401 if none provided", () => {
fetchMock.mock("api/v2/foo", 401);
return apiClient
.get("foo")
.catch(err => {
expect(err.message).toEqual("unauthorized");
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
});
it("should create a normal error, if the content type is not scmm-error", (done) => {
fetchMock.getOnce("/api/v2/error", {
status: 404
});
apiClient.get("/error")
.catch((err: Error) => {
expect(err.name).toEqual("Error");
expect(err.message).toContain("404");
done();
});
});
it("should append error message for 401 if provided", () => {
fetchMock.mock("api/v2/foo", {"status": 401, body: error});
return apiClient
.get("foo")
.catch(err => {
expect(err.message).toEqual("Error!!");
it("should create an backend error, if the content type is scmm-error", (done) => {
fetchMock.getOnce("/api/v2/error", {
status: 404,
headers: {
"Content-Type": "application/vnd.scmm-error+json;v=2"
},
body: earthNotFoundError
});
apiClient.get("/error")
.catch((err: BackendError) => {
expect(err).toBeInstanceOf(BackendError);
expect(err.message).toEqual("earth not found");
expect(err.statusCode).toBe(404);
expect(err.transactionId).toEqual("42t");
expect(err.errorCode).toEqual("42e");
expect(err.context).toEqual([{
type: "planet",
id: "earth"
}]);
done();
});
});
it("should append default error message for 401 if none provided", () => {
fetchMock.mock("api/v2/foo", 404);
return apiClient
.get("foo")
.catch(err => {
expect(err.message).toEqual("not found");
});
});
it("should append error message for 404 if provided", () => {
fetchMock.mock("api/v2/foo", {"status": 404, body: error});
return apiClient
.get("foo")
.catch(err => {
expect(err.message).toEqual("Error!!");
});
});
});
});

View File

@@ -0,0 +1,72 @@
// @flow
type Context = { type: string, id: string }[];
type Violation = { path: string, message: string };
export type BackendErrorContent = {
transactionId: string,
errorCode: string,
message: string,
url?: string,
context: Context,
violations: Violation[]
};
export class BackendError extends Error {
transactionId: string;
errorCode: string;
url: ?string;
context: Context = [];
statusCode: number;
violations: Violation[];
constructor(content: BackendErrorContent, name: string, statusCode: number) {
super(content.message);
this.name = name;
this.transactionId = content.transactionId;
this.errorCode = content.errorCode;
this.url = content.url;
this.context = content.context;
this.statusCode = statusCode;
this.violations = content.violations;
}
}
export class UnauthorizedError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
}
}
export class ForbiddenError extends Error {
statusCode: number;
constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
}
}
export class NotFoundError extends BackendError {
constructor(content: BackendErrorContent, statusCode: number) {
super(content, "NotFoundError", statusCode);
}
}
export function createBackendError(
content: BackendErrorContent,
statusCode: number
) {
switch (statusCode) {
case 404:
return new NotFoundError(content, statusCode);
default:
return new BackendError(content, "BackendError", statusCode);
}
}
export function isBackendError(response: Response) {
return (
response.headers.get("Content-Type") ===
"application/vnd.scmm-error+json;v=2"
);
}

View File

@@ -0,0 +1,35 @@
// @flow
import { BackendError, UnauthorizedError, createBackendError, NotFoundError } from "./errors";
describe("test createBackendError", () => {
const earthNotFoundError = {
transactionId: "42t",
errorCode: "42e",
message: "earth not found",
context: [{
type: "planet",
id: "earth"
}]
};
it("should return a default backend error", () => {
const err = createBackendError(earthNotFoundError, 500);
expect(err).toBeInstanceOf(BackendError);
expect(err.name).toBe("BackendError");
});
it("should return an unauthorized error for status code 403", () => {
const err = createBackendError(earthNotFoundError, 403);
expect(err).toBeInstanceOf(UnauthorizedError);
expect(err.name).toBe("UnauthorizedError");
});
it("should return an not found error for status code 404", () => {
const err = createBackendError(earthNotFoundError, 404);
expect(err).toBeInstanceOf(NotFoundError);
expect(err.name).toBe("NotFoundError");
});
});

View File

@@ -26,7 +26,8 @@ export { getPageFromMatch } from "./urls";
export { default as Autocomplete} from "./Autocomplete";
export { default as BranchSelector } from "./BranchSelector";
export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR, CONFLICT_ERROR } from "./apiclient.js";
export { apiClient } from "./apiclient.js";
export * from "./errors";
export * from "./avatar";
export * from "./buttons";

View File

@@ -23,7 +23,8 @@
"prefix": "Fehler",
"loginLink": "Erneute Anmeldung",
"timeout": "Die Session ist abgelaufen.",
"wrong-login-credentials": "Ungültige Anmeldedaten"
"wrong-login-credentials": "Ungültige Anmeldedaten",
"forbidden": "Sie haben nicht die Berechtigung, diesen Datensatz zu sehen"
},
"loading": {
"alt": "Lade ..."

View File

@@ -9,7 +9,8 @@
"config-form": {
"submit": "Speichern",
"submit-success-notification": "Einstellungen wurden erfolgreich geändert!",
"no-permission-notification": "Hinweis: Es fehlen Berechtigungen zum Bearbeiten der Einstellungen!"
"no-read-permission-notification": "Hinweis: Es fehlen Berechtigungen zum Lesen der Einstellungen!",
"no-write-permission-notification": "Hinweis: Es fehlen Berechtigungen zum Bearbeiten der Einstellungen!"
},
"proxy-settings": {
"name": "Proxy Einstellungen",

View File

@@ -23,7 +23,8 @@
"prefix": "Error",
"loginLink": "You can login here again.",
"timeout": "The session has expired",
"wrong-login-credentials": "Invalid credentials"
"wrong-login-credentials": "Invalid credentials",
"forbidden": "You don't have permission to view this entity"
},
"loading": {
"alt": "Loading ..."

View File

@@ -9,7 +9,8 @@
"config-form": {
"submit": "Submit",
"submit-success-notification": "Configuration changed successfully!",
"no-permission-notification": "Please note: You do not have the permission to edit the config!"
"no-read-permission-notification": "Please note: You do not have the permission to see the config!",
"no-write-permission-notification": "Please note: You do not have the permission to edit the config!"
},
"proxy-settings": {
"name": "Proxy Settings",

View File

@@ -14,6 +14,7 @@ type Props = {
config?: Config,
loading?: boolean,
t: string => string,
configReadPermission: boolean,
configUpdatePermission: boolean
};
@@ -84,16 +85,30 @@ class ConfigForm extends React.Component<Props, State> {
};
render() {
const { loading, t, configUpdatePermission } = this.props;
const {
loading,
t,
configReadPermission,
configUpdatePermission
} = this.props;
const config = this.state.config;
let noPermissionNotification = null;
if (!configReadPermission) {
return (
<Notification
type={"danger"}
children={t("config-form.no-read-permission-notification")}
/>
);
}
if (this.state.showNotification) {
noPermissionNotification = (
<Notification
type={"info"}
children={t("config-form.no-permission-notification")}
children={t("config-form.no-write-permission-notification")}
onClose={() => this.onClose()}
/>
);

View File

@@ -1,7 +1,7 @@
// @flow
import React from "react";
import { translate } from "react-i18next";
import { Title, ErrorPage, Loading } from "@scm-manager/ui-components";
import { Title, Loading, ErrorNotification } from "@scm-manager/ui-components";
import {
fetchConfig,
getFetchConfigFailure,
@@ -35,6 +35,7 @@ type Props = {
};
type State = {
configReadPermission: boolean,
configChanged: boolean
};
@@ -43,13 +44,18 @@ class GlobalConfig extends React.Component<Props, State> {
super(props);
this.state = {
configReadPermission: true,
configChanged: false
};
}
componentDidMount() {
this.props.configReset();
if (this.props.configLink) {
this.props.fetchConfig(this.props.configLink);
} else {
this.setState({configReadPermission: false});
}
}
modifyConfig = (config: Config) => {
@@ -73,18 +79,8 @@ class GlobalConfig extends React.Component<Props, State> {
};
render() {
const { t, error, loading, config, configUpdatePermission } = this.props;
const { t, loading } = this.props;
if (error) {
return (
<ErrorPage
title={t("config.errorTitle")}
subtitle={t("config.errorSubtitle")}
error={error}
configUpdatePermission={configUpdatePermission}
/>
);
}
if (loading) {
return <Loading />;
}
@@ -92,16 +88,39 @@ class GlobalConfig extends React.Component<Props, State> {
return (
<div>
<Title title={t("config.title")} />
{this.renderError()}
{this.renderContent()}
</div>
);
}
renderError = () => {
const { error } = this.props;
if (error) {
return <ErrorNotification error={error} />;
}
return null;
};
renderContent = () => {
const { error, loading, config, configUpdatePermission } = this.props;
const { configReadPermission } = this.state;
if (!error) {
return (
<>
{this.renderConfigChangedNotification()}
<ConfigForm
submitForm={config => this.modifyConfig(config)}
config={config}
loading={loading}
configUpdatePermission={configUpdatePermission}
configReadPermission={configReadPermission}
/>
</div>
</>
);
}
return null;
};
}
const mapDispatchToProps = dispatch => {

View File

@@ -15,8 +15,7 @@ import {
InputField,
SubmitButton,
ErrorNotification,
Image,
UNAUTHORIZED_ERROR
Image, UnauthorizedError
} from "@scm-manager/ui-components";
import classNames from "classnames";
import { getLoginLink } from "../modules/indexResource";
@@ -95,7 +94,7 @@ class Login extends React.Component<Props, State> {
areCredentialsInvalid() {
const { t, error } = this.props;
if (error === UNAUTHORIZED_ERROR) {
if (error instanceof UnauthorizedError) {
return new Error(t("error-notification.wrong-login-credentials"));
} else {
return error;

View File

@@ -54,8 +54,8 @@ export function fetchGroupsByLink(link: string) {
.then(data => {
dispatch(fetchGroupsSuccess(data));
})
.catch(err => {
dispatch(fetchGroupsFailure(link, err));
.catch(error => {
dispatch(fetchGroupsFailure(link, error));
});
};
}
@@ -104,8 +104,8 @@ function fetchGroup(link: string, name: string) {
.then(data => {
dispatch(fetchGroupSuccess(data));
})
.catch(err => {
dispatch(fetchGroupFailure(name, err));
.catch(error => {
dispatch(fetchGroupFailure(name, error));
});
};
}
@@ -149,12 +149,8 @@ export function createGroup(link: string, group: Group, callback?: () => void) {
callback();
}
})
.catch(err => {
dispatch(
createGroupFailure(
err
)
);
.catch(error => {
dispatch(createGroupFailure(error));
});
};
}
@@ -199,13 +195,8 @@ export function modifyGroup(group: Group, callback?: () => void) {
.then(() => {
dispatch(fetchGroupByLink(group));
})
.catch(err => {
dispatch(
modifyGroupFailure(
group,
err
)
);
.catch(error => {
dispatch(modifyGroupFailure(group, error));
});
};
}
@@ -257,8 +248,8 @@ export function deleteGroup(group: Group, callback?: () => void) {
callback();
}
})
.catch(err => {
dispatch(deleteGroupFailure(group, err));
.catch(error => {
dispatch(deleteGroupFailure(group, error));
});
};
}
@@ -342,7 +333,7 @@ function listReducer(state: any = {}, action: any = {}) {
...state,
entries: groupNames,
entry: {
groupCreatePermission: action.payload._links.create ? true : false,
groupCreatePermission: !!action.payload._links.create,
page: action.payload.page,
pageTotal: action.payload.pageTotal,
_links: action.payload._links

View File

@@ -2,7 +2,7 @@
import type { Me } from "@scm-manager/ui-types";
import * as types from "./types";
import { apiClient, UNAUTHORIZED_ERROR } from "@scm-manager/ui-components";
import { apiClient, UnauthorizedError } from "@scm-manager/ui-components";
import { isPending } from "./pending";
import { getFailure } from "./failure";
import {
@@ -152,7 +152,7 @@ export const login = (
dispatch(loginPending());
return apiClient
.post(loginLink, login_data)
.then(response => {
.then(() => {
dispatch(fetchIndexResourcesPending());
return callFetchIndexResources();
})
@@ -178,7 +178,7 @@ export const fetchMe = (link: string) => {
dispatch(fetchMeSuccess(me));
})
.catch((error: Error) => {
if (error === UNAUTHORIZED_ERROR) {
if (error instanceof UnauthorizedError) {
dispatch(fetchMeUnauthenticated());
} else {
dispatch(fetchMeFailure(error));

View File

@@ -179,9 +179,7 @@ describe("auth actions", () => {
});
it("should dispatch fetch me unauthorized", () => {
fetchMock.getOnce("/api/v2/me", {
status: 401
});
fetchMock.getOnce("/api/v2/me", 401);
const expectedActions = [
{ type: FETCH_ME_PENDING },

View File

@@ -12,13 +12,13 @@ import { Route, Switch } from "react-router-dom";
import type { Repository } from "@scm-manager/ui-types";
import {
ErrorPage,
CollapsibleErrorPage,
Loading,
Navigation,
SubNavigation,
NavLink,
Page,
Section
Section, ErrorPage
} from "@scm-manager/ui-components";
import { translate } from "react-i18next";
import RepositoryDetails from "../components/RepositoryDetails";
@@ -82,13 +82,11 @@ class RepositoryRoot extends React.Component<Props> {
const { loading, error, indexLinks, repository, t } = this.props;
if (error) {
return (
<ErrorPage
return <ErrorPage
title={t("repositoryRoot.errorTitle")}
subtitle={t("repositoryRoot.errorSubtitle")}
error={error}
/>
);
}
if (!repository || loading) {

View File

@@ -229,8 +229,8 @@ export function modifyRepo(repository: Repository, callback?: () => void) {
.then(() => {
dispatch(fetchRepoByLink(repository));
})
.catch(err => {
dispatch(modifyRepoFailure(repository, err));
.catch(error => {
dispatch(modifyRepoFailure(repository, error));
});
};
}

View File

@@ -37,10 +37,7 @@ function fetchRepositoryTypes(dispatch: any) {
.then(repositoryTypes => {
dispatch(fetchRepositoryTypesSuccess(repositoryTypes));
})
.catch(err => {
const error = new Error(
`failed to fetch repository types: ${err.message}`
);
.catch(error => {
dispatch(fetchRepositoryTypesFailure(error));
});
}

View File

@@ -35,6 +35,8 @@ export const DELETE_USER_FAILURE = `${DELETE_USER}_${types.FAILURE_SUFFIX}`;
const CONTENT_TYPE_USER = "application/vnd.scmm-user+json;v=2";
// TODO i18n for error messages
// fetch users
export function fetchUsers(link: string) {
@@ -55,8 +57,8 @@ export function fetchUsersByLink(link: string) {
.then(data => {
dispatch(fetchUsersSuccess(data));
})
.catch(err => {
dispatch(fetchUsersFailure(link, err));
.catch(error => {
dispatch(fetchUsersFailure(link, error));
});
};
}
@@ -105,8 +107,8 @@ function fetchUser(link: string, name: string) {
.then(data => {
dispatch(fetchUserSuccess(data));
})
.catch(err => {
dispatch(fetchUserFailure(name, err));
.catch(error => {
dispatch(fetchUserFailure(name, error));
});
};
}
@@ -151,7 +153,9 @@ export function createUser(link: string, user: User, callback?: () => void) {
callback();
}
})
.catch(err => dispatch(createUserFailure(err)));
.catch(error =>
dispatch(createUserFailure(error))
);
};
}
@@ -250,8 +254,8 @@ export function deleteUser(user: User, callback?: () => void) {
callback();
}
})
.catch(err => {
dispatch(deleteUserFailure(user, err));
.catch(error => {
dispatch(deleteUserFailure(user, error));
});
};
}

View File

@@ -2,9 +2,9 @@ package sonia.scm.api.v2;
import org.jboss.resteasy.api.validation.ResteasyViolationException;
import sonia.scm.api.v2.resources.ResteasyViolationExceptionToErrorDtoMapper;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@@ -23,7 +23,7 @@ public class ResteasyValidationExceptionMapper implements ExceptionMapper<Restea
public Response toResponse(ResteasyViolationException exception) {
return Response
.status(Response.Status.BAD_REQUEST)
.type(MediaType.APPLICATION_JSON_TYPE)
.type(VndMediaType.ERROR_TYPE)
.entity(mapper.map(exception))
.build();
}

View File

@@ -2,9 +2,9 @@ package sonia.scm.api.v2;
import sonia.scm.ScmConstraintViolationException;
import sonia.scm.api.v2.resources.ScmViolationExceptionToErrorDtoMapper;
import sonia.scm.web.VndMediaType;
import javax.inject.Inject;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;
@@ -23,7 +23,7 @@ public class ScmConstraintValidationExceptionMapper implements ExceptionMapper<S
public Response toResponse(ScmConstraintViolationException exception) {
return Response
.status(Response.Status.BAD_REQUEST)
.type(MediaType.APPLICATION_JSON_TYPE)
.type(VndMediaType.ERROR_TYPE)
.entity(mapper.map(exception))
.build();
}

View File

@@ -93,5 +93,55 @@
"description": "Darf im Repository Kontext alles ausführen. Dies beinhaltet alle Repository Berechtigungen."
}
}
},
"errors": {
"context": "Kontext",
"errorCode": "Fehlercode",
"transactionId": "Transaktions-ID",
"moreInfo": "Für mehr Informationen, siehe",
"AGR7UzkhA1": {
"displayName": "Nicht gefunden",
"description": "Der gewünschte Datensatz konnte nicht gefunden werden. Möglicherweise wurde er in einer weiteren Session gelöscht."
},
"FtR7UznKU1": {
"displayName": "Existiert bereits",
"description": "Ein Datensatz mit den gegebenen Schlüsselwerten existiert bereits"
},
"9BR7qpDAe1": {
"displayName": "Passwortänderung nicht erlaubt",
"description": "Sie haben nicht die Berechtigung, das Passwort zu ändern"
},
"2wR7UzpPG1": {
"displayName": "Konkurrierende Änderungen",
"description": "Der Datensatz wurde konkurrierend von einem anderen Benutzer oder einem anderen Prozess modifiziert. Bitte laden sie die Daten erneut."
},
"9SR8G0kmU1": {
"displayName": "Feature nicht unterstützt",
"description": "Das Versionsverwaltungssystem dieses Repositories unterstützt das angefragte Feature nicht."
},
"CmR8GCJb31": {
"displayName": "Interner Serverfehler",
"description": "Im Server ist ein interner Fehler aufgetreten. Bitte wenden Sie sich an ihren Administrator für weitere Hinweise."
},
"92RCCCMHO1": {
"displayName": "Eine interne URL wurde nicht gefunden",
"description": "Ein interner Serveraufruf konnte nicht verarbeitet werden. Bitte wenden Sie sich an ihren Administrator für weitere Hinweise."
},
"2VRCrvpL71": {
"displayName": "Ungültiges Datenformat",
"description": "Die zum Server gesendeten Daten konnten nicht verarbeitet werden. Bitte prüfen Sie die eingegebenen Werte oder wenden Sie sich an ihren Administrator für weitere Hinweise."
},
"8pRBYDURx1": {
"displayName": "Ungültiger Datentyp",
"description": "Die zum Server gesendeten Daten hatten einen ungültigen Typen. Bitte wenden Sie sich an ihren Administrator für weitere Hinweise."
},
"1wR7ZBe7H1": {
"displayName": "Ungültige Eingabe",
"description": "Die eingegebenen Daten konnten nicht validiert werden. Bitte korrigieren Sie die Eingaben und senden Sie sie erneut."
},
"3zR9vPNIE1": {
"displayName": "Ungültige Eingabe",
"description": "Die eingegebenen Daten konnten nicht validiert werden. Bitte korrigieren Sie die Eingaben und senden Sie sie erneut."
}
}
}

View File

@@ -93,5 +93,55 @@
"description": "May change everything for the repository (includes all other permissions)"
}
}
},
"errors": {
"context": "Context",
"errorCode": "Error Code",
"transactionId": "Transaction ID",
"moreInfo": "For more information, see",
"AGR7UzkhA1": {
"displayName": "Not found",
"description": "The requested entity could not be found. It may have been deleted in another session."
},
"FtR7UznKU1": {
"displayName": "Already exists",
"description": "There is already an entity with the same key values."
},
"9BR7qpDAe1": {
"displayName": "Password change not allowed",
"description": "You do not have the permission to change the password."
},
"2wR7UzpPG1": {
"displayName": "Concurrent modifications",
"description": "The entity has been modified concurrently by another user or another process. Please reload the entity."
},
"9SR8G0kmU1": {
"displayName": "Feature not supported",
"description": "The version control system for this repository does not support the requested feature."
},
"CmR8GCJb31": {
"displayName": "Internal server error",
"description": "The server encountered an internal error. Please contact your administrator for further assistance."
},
"92RCCCMHO1": {
"displayName": "An internal URL could not be found",
"description": "An internal request could not be handled by the server. Please contact your administrator for further assistance."
},
"2VRCrvpL71": {
"displayName": "Illegal data format",
"description": "The data sent to the server could not be handled. Please check the values you have entered or contact your administrator for further assistance."
},
"8pRBYDURx1": {
"displayName": "Illegal data type",
"description": "The data sent to the server had an illegal data type. Please contact your administrator for further assistance."
},
"1wR7ZBe7H1": {
"displayName": "Illegal input",
"description": "The values could not be validated. Please correct your input and try again."
},
"3zR9vPNIE1": {
"displayName": "Illegal input",
"description": "The values could not be validated. Please correct your input and try again."
}
}
}