diff --git a/scm-ui-components/packages/ui-components/src/BackendErrorNotification.js b/scm-ui-components/packages/ui-components/src/BackendErrorNotification.js new file mode 100644 index 0000000000..31106593c4 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/BackendErrorNotification.js @@ -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 { + constructor(props: Props) { + super(props); + } + + render() { + return ( + +
+

{this.renderErrorName()}

+

{this.renderErrorDescription()}

+

{this.renderViolations()}

+ {this.renderMetadata()} +
+
+ ); + } + + 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 ( + <> +

+ {t("errors.violations")} +

+ + + ); + } + }; + + renderMetadata = () => { + const { error, t } = this.props; + return ( + <> + {this.renderContext()} + {this.renderMoreInformationLink()} +
+
+ {t("errors.transactionId")} {error.transactionId} +
+
+ {t("errors.errorCode")} {error.errorCode} +
+
+ + ); + }; + + renderContext = () => { + const { error, t} = this.props; + if (error.context) { + return ( + <> +

+ {t("errors.context")} +

+ + + ); + } + }; + + renderMoreInformationLink = () => { + const { error, t } = this.props; + if (error.url) { + return ( +

+ {t("errors.moreInfo")}{" "} + + {error.errorCode} + +

+ ); + } + }; +} + +export default translate("plugins")(BackendErrorNotification); diff --git a/scm-ui-components/packages/ui-components/src/ErrorNotification.js b/scm-ui-components/packages/ui-components/src/ErrorNotification.js index 6645db5f60..b8acf89733 100644 --- a/scm-ui-components/packages/ui-components/src/ErrorNotification.js +++ b/scm-ui-components/packages/ui-components/src/ErrorNotification.js @@ -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 { +class ErrorNotification extends React.Component { render() { const { t, error } = this.props; if (error) { - if (error === UNAUTHORIZED_ERROR) { + if (error instanceof BackendError) { + return + } else if (error instanceof UnauthorizedError) { return ( - {t("error-notification.prefix")}: {t("error-notification.timeout")} - {" "} - {t("error-notification.loginLink")} + {t("error-notification.prefix")}:{" "} + {t("error-notification.timeout")}{" "} + + {t("error-notification.loginLink")} + ); - } else { + } else if (error instanceof ForbiddenError) { + return ( + + {t("error-notification.prefix")}:{" "} + {t("error-notification.forbidden")} + + ) + } else + { return ( {t("error-notification.prefix")}: {error.message} @@ -34,4 +47,4 @@ class ErrorNotification extends React.Component { } } -export default translate("commons")(ErrorNotification); +export default translate("commons")(ErrorNotification); diff --git a/scm-ui-components/packages/ui-components/src/ErrorPage.js b/scm-ui-components/packages/ui-components/src/ErrorPage.js index 196319681c..b86f374263 100644 --- a/scm-ui-components/packages/ui-components/src/ErrorPage.js +++ b/scm-ui-components/packages/ui-components/src/ErrorPage.js @@ -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 { render() { - const { title, subtitle, error } = this.props; + const { title, error } = this.props; return (

{title}

-

{subtitle}

+ {this.renderSubtitle()}
); } + + renderSubtitle = () => { + const { error, subtitle } = this.props; + if (error instanceof BackendError || error instanceof ForbiddenError) { + return null; + } + return

{subtitle}

+ } } export default ErrorPage; diff --git a/scm-ui-components/packages/ui-components/src/apiclient.js b/scm-ui-components/packages/ui-components/src/apiclient.js index 25a108877a..77ebe8cf4e 100644 --- a/scm-ui-components/packages/ui-components/src/apiclient.js +++ b/scm-ui-components/packages/ui-components/src/apiclient.js @@ -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) { - 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); - } + +function handleFailure(response: Response) { + if (!response.ok) { + 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 { - 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 { @@ -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); } } diff --git a/scm-ui-components/packages/ui-components/src/apiclient.test.js b/scm-ui-components/packages/ui-components/src/apiclient.test.js index bf3358fe95..8827ca72e4 100644 --- a/scm-ui-components/packages/ui-components/src/apiclient.test.js +++ b/scm-ui-components/packages/ui-components/src/apiclient.test.js @@ -1,65 +1,78 @@ // @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("create url", () => { + it("should not change absolute urls", () => { + expect(createUrl("https://www.scm-manager.org")).toBe( + "https://www.scm-manager.org" + ); + }); + + it("should add prefix for api", () => { + expect(createUrl("/users")).toBe("/api/v2/users"); + expect(createUrl("users")).toBe("/api/v2/users"); + }); +}); + + +describe("error handling tests", () => { + + const earthNotFoundError = { + transactionId: "42t", + errorCode: "42e", + message: "earth not found", + context: [{ + type: "planet", + id: "earth" + }] + }; -describe("apiClient", () => { afterEach(() => { fetchMock.reset(); fetchMock.restore(); }); - describe("create url", () => { - it("should not change absolute urls", () => { - expect(createUrl("https://www.scm-manager.org")).toBe( - "https://www.scm-manager.org" - ); + it("should create a normal error, if the content type is not scmm-error", (done) => { + + fetchMock.getOnce("/api/v2/error", { + status: 404 }); - it("should add prefix for api", () => { - expect(createUrl("/users")).toBe("/api/v2/users"); - expect(createUrl("users")).toBe("/api/v2/users"); - }); + apiClient.get("/error") + .catch((err: Error) => { + expect(err.name).toEqual("Error"); + expect(err.message).toContain("404"); + done(); + }); }); - describe("error handling", () => { - const error = { - message: "Error!!" - }; - - 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"); - }); + 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 }); - 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!!"); - }); - }); + apiClient.get("/error") + .catch((err: BackendError) => { - 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"); - }); - }); + expect(err).toBeInstanceOf(BackendError); - 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!!"); - }); - }); + 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(); + }); }); + }); diff --git a/scm-ui-components/packages/ui-components/src/errors.js b/scm-ui-components/packages/ui-components/src/errors.js new file mode 100644 index 0000000000..13e4996cf7 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/errors.js @@ -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" + ); +} diff --git a/scm-ui-components/packages/ui-components/src/errors.test.js b/scm-ui-components/packages/ui-components/src/errors.test.js new file mode 100644 index 0000000000..3a886e7143 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/errors.test.js @@ -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"); + }); + +}); diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index 5b3cdb4c95..b403ea78b7 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -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"; diff --git a/scm-ui/public/locales/de/commons.json b/scm-ui/public/locales/de/commons.json index f32d764ce8..51a493fc40 100644 --- a/scm-ui/public/locales/de/commons.json +++ b/scm-ui/public/locales/de/commons.json @@ -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 ..." diff --git a/scm-ui/public/locales/de/config.json b/scm-ui/public/locales/de/config.json index 5767a2b376..b67bf90262 100644 --- a/scm-ui/public/locales/de/config.json +++ b/scm-ui/public/locales/de/config.json @@ -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", diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index fed749a200..cc4edde82c 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -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 ..." diff --git a/scm-ui/public/locales/en/config.json b/scm-ui/public/locales/en/config.json index b08c5c2d1b..1b42878015 100644 --- a/scm-ui/public/locales/en/config.json +++ b/scm-ui/public/locales/en/config.json @@ -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", diff --git a/scm-ui/src/config/components/form/ConfigForm.js b/scm-ui/src/config/components/form/ConfigForm.js index dc3f20c95d..7b650ccbfd 100644 --- a/scm-ui/src/config/components/form/ConfigForm.js +++ b/scm-ui/src/config/components/form/ConfigForm.js @@ -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 { }; 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 ( + + ); + } + if (this.state.showNotification) { noPermissionNotification = ( this.onClose()} /> ); diff --git a/scm-ui/src/config/containers/GlobalConfig.js b/scm-ui/src/config/containers/GlobalConfig.js index eac8e27bee..fd3ee04098 100644 --- a/scm-ui/src/config/containers/GlobalConfig.js +++ b/scm-ui/src/config/containers/GlobalConfig.js @@ -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 { super(props); this.state = { + configReadPermission: true, configChanged: false }; } componentDidMount() { this.props.configReset(); - this.props.fetchConfig(this.props.configLink); + 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 { }; render() { - const { t, error, loading, config, configUpdatePermission } = this.props; + const { t, loading } = this.props; - if (error) { - return ( - - ); - } if (loading) { return ; } @@ -92,16 +88,39 @@ class GlobalConfig extends React.Component { return (
- {this.renderConfigChangedNotification()} - <ConfigForm - submitForm={config => this.modifyConfig(config)} - config={config} - loading={loading} - configUpdatePermission={configUpdatePermission} - /> + {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} + /> + </> + ); + } + return null; + }; } const mapDispatchToProps = dispatch => { diff --git a/scm-ui/src/containers/Login.js b/scm-ui/src/containers/Login.js index 3ee9b141ef..4ae9b5a622 100644 --- a/scm-ui/src/containers/Login.js +++ b/scm-ui/src/containers/Login.js @@ -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; diff --git a/scm-ui/src/groups/modules/groups.js b/scm-ui/src/groups/modules/groups.js index 483b5b3798..bbaccf6c4a 100644 --- a/scm-ui/src/groups/modules/groups.js +++ b/scm-ui/src/groups/modules/groups.js @@ -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 diff --git a/scm-ui/src/modules/auth.js b/scm-ui/src/modules/auth.js index 5d15107406..4c0d22b23d 100644 --- a/scm-ui/src/modules/auth.js +++ b/scm-ui/src/modules/auth.js @@ -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)); diff --git a/scm-ui/src/modules/auth.test.js b/scm-ui/src/modules/auth.test.js index 7236f803a8..1d7cb5096e 100644 --- a/scm-ui/src/modules/auth.test.js +++ b/scm-ui/src/modules/auth.test.js @@ -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 }, diff --git a/scm-ui/src/repos/containers/RepositoryRoot.js b/scm-ui/src/repos/containers/RepositoryRoot.js index 8608b9c957..08d6aa9380 100644 --- a/scm-ui/src/repos/containers/RepositoryRoot.js +++ b/scm-ui/src/repos/containers/RepositoryRoot.js @@ -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 - title={t("repositoryRoot.errorTitle")} - subtitle={t("repositoryRoot.errorSubtitle")} - error={error} - /> - ); + return <ErrorPage + title={t("repositoryRoot.errorTitle")} + subtitle={t("repositoryRoot.errorSubtitle")} + error={error} + /> } if (!repository || loading) { diff --git a/scm-ui/src/repos/modules/repos.js b/scm-ui/src/repos/modules/repos.js index aa77b4553b..fa89dc42a6 100644 --- a/scm-ui/src/repos/modules/repos.js +++ b/scm-ui/src/repos/modules/repos.js @@ -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)); }); }; } diff --git a/scm-ui/src/repos/modules/repositoryTypes.js b/scm-ui/src/repos/modules/repositoryTypes.js index 2cdbd1194d..d96d24b612 100644 --- a/scm-ui/src/repos/modules/repositoryTypes.js +++ b/scm-ui/src/repos/modules/repositoryTypes.js @@ -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)); }); } diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index ab330d9ffd..a93d082f58 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -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)); }); }; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/ResteasyValidationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/ResteasyValidationExceptionMapper.java index 63582e10b8..0b8b44f308 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/ResteasyValidationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/ResteasyValidationExceptionMapper.java @@ -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(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java index 991aeedaeb..17883e41cb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/ScmConstraintValidationExceptionMapper.java @@ -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(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index 71b1127ad8..d19c1a9e03 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -80,8 +80,6 @@ public class BranchRootResource { .build(); } catch (CommandNotSupportedException ex) { return Response.status(Response.Status.BAD_REQUEST).build(); - } catch (NotFoundException e) { - return Response.status(Response.Status.NOT_FOUND).build(); } } diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index 6c6a383fe5..0822be9cdd 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -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." + } } } diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 7efcbbe39c..cc9902565b 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -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." + } } }