From a4d78e6e60c262e4058ac63b9eaafae68eab7102 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Thu, 15 Nov 2018 21:39:08 +0100 Subject: [PATCH 01/47] improve error handling in the ui --- .../packages/ui-components/package.json | 3 +- .../ui-components/src/ErrorNotification.js | 64 +++++++++++++++++- .../packages/ui-components/src/apiclient.js | 33 ++++++---- .../ui-components/src/apiclient.test.js | 65 ++++++++++++++++++- .../packages/ui-components/src/errors.js | 53 +++++++++++++++ .../packages/ui-components/src/errors.test.js | 35 ++++++++++ .../packages/ui-components/src/index.js | 3 +- .../packages/ui-components/yarn.lock | 23 ++++++- scm-ui/src/groups/modules/groups.js | 26 ++------ scm-ui/src/modules/auth.js | 6 +- scm-ui/src/repos/modules/repos.js | 3 +- scm-ui/src/repos/modules/repositoryTypes.js | 5 +- scm-ui/src/users/modules/users.js | 19 ++---- 13 files changed, 275 insertions(+), 63 deletions(-) create mode 100644 scm-ui-components/packages/ui-components/src/errors.js create mode 100644 scm-ui-components/packages/ui-components/src/errors.test.js diff --git a/scm-ui-components/packages/ui-components/package.json b/scm-ui-components/packages/ui-components/package.json index 823ca7143e..4a4b4dc82e 100644 --- a/scm-ui-components/packages/ui-components/package.json +++ b/scm-ui-components/packages/ui-components/package.json @@ -18,6 +18,7 @@ "create-index": "^2.3.0", "enzyme": "^3.5.0", "enzyme-adapter-react-16": "^1.3.1", + "fetch-mock": "^7.2.5", "flow-bin": "^0.79.1", "flow-typed": "^2.5.1", "jest": "^23.5.0", @@ -55,4 +56,4 @@ ] ] } -} \ No newline at end of file +} diff --git a/scm-ui-components/packages/ui-components/src/ErrorNotification.js b/scm-ui-components/packages/ui-components/src/ErrorNotification.js index 9ef3b58653..6a0dc202eb 100644 --- a/scm-ui-components/packages/ui-components/src/ErrorNotification.js +++ b/scm-ui-components/packages/ui-components/src/ErrorNotification.js @@ -2,6 +2,7 @@ import React from "react"; import { translate } from "react-i18next"; import Notification from "./Notification"; +import { BackendError } from "./errors"; type Props = { t: string => string, @@ -9,12 +10,71 @@ type Props = { }; class ErrorNotification extends React.Component { + + renderMoreInformationsLink(error: BackendError) { + if (error.url) { + // TODO i18n + return ( +

+ For more informations, see {error.errorCode} +

+ ); + } + } + + renderBackendError(error: BackendError) { + // TODO i18n + // how should we handle i18n for the message? + // we could add translation for known error codes to i18n and pass the context objects as parameters, + // but this will not work for errors from plugins, because the ErrorNotification will search for the translation + // in the wrong namespace (plugins could only add translations to the plugins namespace. + // should we add a special namespace for errors? which could be extend by plugins? + + // TODO error page + // we have currently the ErrorNotification, which is often wrapped by the ErrorPage + // the ErrorPage has often a SubTitle like "Unkwown xzy error", which is no longer always the case + // if the error is a BackendError its not fully unknown + return ( +
+

{error.message}

+

Context:

+
    + {error.context.map((context, index) => { + return ( +
  • + {context.type}: {context.id} +
  • + ); + })} +
+ { this.renderMoreInformationsLink(error) } +
+
+ ErrorCode: {error.errorCode} +
+
+ TransactionId: {error.transactionId} +
+
+
+ ); + } + + renderError(error: Error) { + if (error instanceof BackendError) { + return this.renderBackendError(error); + } else { + return error.message; + } + } + render() { - const { t, error } = this.props; + const { error } = this.props; + if (error) { return ( - {t("error-notification.prefix")}: {error.message} + {this.renderError(error)} ); } diff --git a/scm-ui-components/packages/ui-components/src/apiclient.js b/scm-ui-components/packages/ui-components/src/apiclient.js index bd19dcdf14..348371fddd 100644 --- a/scm-ui-components/packages/ui-components/src/apiclient.js +++ b/scm-ui-components/packages/ui-components/src/apiclient.js @@ -1,8 +1,7 @@ // @flow import { contextPath } from "./urls"; - -export const NOT_FOUND_ERROR = Error("not found"); -export const UNAUTHORIZED_ERROR = Error("unauthorized"); +import { createBackendError } from "./errors"; +import type { BackendErrorContent } from "./errors"; const fetchOptions: RequestOptions = { credentials: "same-origin", @@ -11,15 +10,21 @@ const fetchOptions: RequestOptions = { } }; -function handleStatusCode(response: Response) { + +function isBackendError(response) { + return response.headers.get("Content-Type") === "application/vnd.scmm-error+json;v=2"; +} + +function handleFailure(response: Response) { if (!response.ok) { - if (response.status === 401) { - throw UNAUTHORIZED_ERROR; + if (isBackendError(response)) { + return response.json() + .then((content: BackendErrorContent) => { + throw createBackendError(content, response.status); + }); + } else { + throw new Error("server returned status code " + response.status); } - if (response.status === 404) { - throw NOT_FOUND_ERROR; - } - throw new Error("server returned status code " + response.status); } return response; } @@ -37,7 +42,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") { @@ -53,7 +58,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 { @@ -61,7 +66,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( @@ -78,7 +83,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 deb22a3b54..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,5 +1,7 @@ // @flow -import { createUrl } from "./apiclient"; +import { apiClient, createUrl } from "./apiclient"; +import {fetchMock} from "fetch-mock"; +import { BackendError } from "./errors"; describe("create url", () => { it("should not change absolute urls", () => { @@ -13,3 +15,64 @@ describe("create url", () => { 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" + }] + }; + + 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 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(); + }); + }); + +}); 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..8becec8a7e --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/errors.js @@ -0,0 +1,53 @@ +// @flow +type Context = {type: string, id: string}[]; + +export type BackendErrorContent = { + transactionId: string, + errorCode: string, + message: string, + url?: string, + context: Context +}; + +export class BackendError extends Error { + + transactionId: string; + errorCode: string; + url: ?string; + context: Context = []; + statusCode: number; + + 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; + } + +} + +export class UnauthorizedError extends BackendError { + constructor(content: BackendErrorContent, statusCode: number) { + super(content, "UnauthorizedError", 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 403: + return new UnauthorizedError(content, statusCode); + case 404: + return new NotFoundError(content, statusCode); + default: + return new BackendError(content, "BackendError", statusCode); + } +} 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 41e385af8d..9dc4af1aac 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -22,7 +22,8 @@ export { default as HelpIcon } from "./HelpIcon"; export { default as Tooltip } from "./Tooltip"; export { getPageFromMatch } from "./urls"; -export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js"; +export { apiClient } from "./apiclient.js"; +export * from "./errors"; export * from "./buttons"; export * from "./config"; diff --git a/scm-ui-components/packages/ui-components/yarn.lock b/scm-ui-components/packages/ui-components/yarn.lock index f11cfa5bcd..d7afe51035 100644 --- a/scm-ui-components/packages/ui-components/yarn.lock +++ b/scm-ui-components/packages/ui-components/yarn.lock @@ -688,6 +688,10 @@ react "^16.4.2" react-dom "^16.4.2" +"@scm-manager/ui-types@2.0.0-SNAPSHOT": + version "2.0.0-20181010-130547" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-types/-/ui-types-2.0.0-20181010-130547.tgz#9987b519e43d5c4b895327d012d3fd72429a7953" + "@types/node@*": version "10.12.0" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.0.tgz#ea6dcbddbc5b584c83f06c60e82736d8fbb0c235" @@ -2995,6 +2999,15 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +fetch-mock@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/fetch-mock/-/fetch-mock-7.2.5.tgz#4682f51b9fa74d790e10a471066cb22f3ff84d48" + dependencies: + babel-polyfill "^6.26.0" + glob-to-regexp "^0.4.0" + path-to-regexp "^2.2.1" + whatwg-url "^6.5.0" + figures@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" @@ -3341,6 +3354,10 @@ glob-stream@^3.1.5: through2 "^0.6.1" unique-stream "^1.0.0" +glob-to-regexp@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.0.tgz#49bd677b1671022bd10921c3788f23cdebf9c7e6" + glob-watcher@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/glob-watcher/-/glob-watcher-0.0.6.tgz#b95b4a8df74b39c83298b0c05c978b4d9a3b710b" @@ -5982,6 +5999,10 @@ path-to-regexp@^1.7.0: dependencies: isarray "0.0.1" +path-to-regexp@^2.2.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.4.0.tgz#35ce7f333d5616f1c1e1bfe266c3aba2e5b2e704" + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -7814,7 +7835,7 @@ whatwg-mimetype@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.2.0.tgz#a3d58ef10b76009b042d03e25591ece89b88d171" -whatwg-url@^6.4.1: +whatwg-url@^6.4.1, whatwg-url@^6.5.0: version "6.5.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" dependencies: diff --git a/scm-ui/src/groups/modules/groups.js b/scm-ui/src/groups/modules/groups.js index 165648edaa..ad8676fc7b 100644 --- a/scm-ui/src/groups/modules/groups.js +++ b/scm-ui/src/groups/modules/groups.js @@ -54,8 +54,7 @@ export function fetchGroupsByLink(link: string) { .then(data => { dispatch(fetchGroupsSuccess(data)); }) - .catch(cause => { - const error = new Error(`could not fetch groups: ${cause.message}`); + .catch(error => { dispatch(fetchGroupsFailure(link, error)); }); }; @@ -105,8 +104,7 @@ function fetchGroup(link: string, name: string) { .then(data => { dispatch(fetchGroupSuccess(data)); }) - .catch(cause => { - const error = new Error(`could not fetch group: ${cause.message}`); + .catch(error => { dispatch(fetchGroupFailure(name, error)); }); }; @@ -152,11 +150,7 @@ export function createGroup(link: string, group: Group, callback?: () => void) { } }) .catch(error => { - dispatch( - createGroupFailure( - new Error(`Failed to create group ${group.name}: ${error.message}`) - ) - ); + dispatch(createGroupFailure(error)); }); }; } @@ -201,13 +195,8 @@ export function modifyGroup(group: Group, callback?: () => void) { .then(() => { dispatch(fetchGroupByLink(group)); }) - .catch(cause => { - dispatch( - modifyGroupFailure( - group, - new Error(`could not modify group ${group.name}: ${cause.message}`) - ) - ); + .catch(error => { + dispatch(modifyGroupFailure(group, error)); }); }; } @@ -259,10 +248,7 @@ export function deleteGroup(group: Group, callback?: () => void) { callback(); } }) - .catch(cause => { - const error = new Error( - `could not delete group ${group.name}: ${cause.message}` - ); + .catch(error => { dispatch(deleteGroupFailure(group, error)); }); }; diff --git a/scm-ui/src/modules/auth.js b/scm-ui/src/modules/auth.js index 691ae2b128..6dcd03847b 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 { @@ -159,7 +159,7 @@ export const login = ( dispatch(loginPending()); return apiClient .post(loginLink, login_data) - .then(response => { + .then(() => { dispatch(fetchIndexResourcesPending()); return callFetchIndexResources(); }) @@ -185,7 +185,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/repos/modules/repos.js b/scm-ui/src/repos/modules/repos.js index 642f6cf395..dd5985d444 100644 --- a/scm-ui/src/repos/modules/repos.js +++ b/scm-ui/src/repos/modules/repos.js @@ -224,8 +224,7 @@ export function modifyRepo(repository: Repository, callback?: () => void) { .then(() => { dispatch(fetchRepoByLink(repository)); }) - .catch(cause => { - const error = new Error(`failed to modify repo: ${cause.message}`); + .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 fe751d13d4..a93d082f58 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -57,8 +57,7 @@ export function fetchUsersByLink(link: string) { .then(data => { dispatch(fetchUsersSuccess(data)); }) - .catch(cause => { - const error = new Error(`could not fetch users: ${cause.message}`); + .catch(error => { dispatch(fetchUsersFailure(link, error)); }); }; @@ -108,8 +107,7 @@ function fetchUser(link: string, name: string) { .then(data => { dispatch(fetchUserSuccess(data)); }) - .catch(cause => { - const error = new Error(`could not fetch user: ${cause.message}`); + .catch(error => { dispatch(fetchUserFailure(name, error)); }); }; @@ -155,12 +153,8 @@ export function createUser(link: string, user: User, callback?: () => void) { callback(); } }) - .catch(err => - dispatch( - createUserFailure( - new Error(`failed to add user ${user.name}: ${err.message}`) - ) - ) + .catch(error => + dispatch(createUserFailure(error)) ); }; } @@ -260,10 +254,7 @@ export function deleteUser(user: User, callback?: () => void) { callback(); } }) - .catch(cause => { - const error = new Error( - `could not delete user ${user.name}: ${cause.message}` - ); + .catch(error => { dispatch(deleteUserFailure(user, error)); }); }; From a7f469985373f3d4c94378032878f29ce55c3a70 Mon Sep 17 00:00:00 2001 From: Mohamed Karray Date: Thu, 21 Feb 2019 17:08:23 +0100 Subject: [PATCH 02/47] add extension point to the content ui --- scm-ui/src/repos/sources/containers/Content.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js index 2f3f5ba853..9eb1bc9172 100644 --- a/scm-ui/src/repos/sources/containers/Content.js +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -11,6 +11,7 @@ import SourcesView from "./SourcesView"; import HistoryView from "./HistoryView"; import { getSources } from "../modules/sources"; import { connect } from "react-redux"; +import {ExtensionPoint} from "@scm-manager/ui-extensions"; type Props = { loading: boolean, @@ -148,6 +149,11 @@ class Content extends React.Component { {t("sources.content.description")} {description} + + From 40da16c451c529b92fd3fccc7274681d88afc45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 25 Feb 2019 11:41:00 +0100 Subject: [PATCH 03/47] Clarify permission texts --- .../src/main/resources/locales/de/plugins.json | 6 +++--- .../src/main/resources/locales/en/plugins.json | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index def3d0b093..8c2015a0e8 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -55,15 +55,15 @@ "verbs": { "repository": { "read": { - "displayName": "Lesen", + "displayName": "Repository Lesen", "description": "Darf das Repository im SCM-Manager sehen." }, "modify": { - "displayName": "Modifizieren", + "displayName": "Repository Modifizieren", "description": "Darf die Eigenschaften des Repository verändern." }, "delete": { - "displayName": "Löschen", + "displayName": "Repository Löschen", "description": "Darf das Repository löschen." }, "pull": { diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index bf771def44..03eb06e597 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -55,23 +55,23 @@ "verbs": { "repository": { "read": { - "displayName": "read", + "displayName": "read repository", "description": "May see the repository inside the SCM-Manager" }, "modify": { - "displayName": "modify", - "description": "May modify the properties of the repository" + "displayName": "modify repository metadata", + "description": "May modify the basic properties of the repository" }, "delete": { - "displayName": "delete", + "displayName": "delete repository", "description": "May delete the repository" }, "pull": { - "displayName": "pull/checkout", + "displayName": "pull/checkout repository", "description": "May pull/checkout the repository" }, "push": { - "displayName": "push/commit", + "displayName": "push/commit repository", "description": "May change the content of the repository (push/commit)" }, "permissionRead": { @@ -83,7 +83,7 @@ "description": "May modify the permissions of the repository" }, "*": { - "displayName": "overall", + "displayName": "own repository", "description": "May change everything for the repository (includes all other permissions)" } } From fbb0d2cf34aa5c46223102a7601354abb6c2e3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Pfeuffer?= Date: Mon, 25 Feb 2019 14:16:45 +0100 Subject: [PATCH 04/47] Fix create button visible for unprivileged user --- scm-ui/src/groups/containers/Groups.js | 20 ++++++++++++-------- scm-ui/src/repos/containers/Overview.js | 21 +++++++++++++-------- scm-ui/src/users/containers/Users.js | 20 ++++++++++++-------- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index 7bc31a8b79..044cbfdb08 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -78,13 +78,6 @@ class Groups extends React.Component { {this.renderPaginator()} {this.renderCreateButton()} - -