diff --git a/scm-core/src/main/java/sonia/scm/Manager.java b/scm-core/src/main/java/sonia/scm/Manager.java index 9a7f21d3ef..6e0b72295c 100644 --- a/scm-core/src/main/java/sonia/scm/Manager.java +++ b/scm-core/src/main/java/sonia/scm/Manager.java @@ -35,6 +35,7 @@ package sonia.scm; import java.util.Collection; import java.util.Comparator; +import java.util.function.Predicate; /** * Base interface for all manager classes. @@ -82,11 +83,12 @@ public interface Manager * Returns all object of the store sorted by the given {@link java.util.Comparator} * * + * @param filter to filter the returned objects * @param comparator to sort the returned objects * @since 1.4 * @return all object of the store sorted by the given {@link java.util.Comparator} */ - Collection getAll(Comparator comparator); + Collection getAll(Predicate filter, Comparator comparator); /** * Returns objects from the store which are starts at the given start @@ -125,6 +127,7 @@ public interface Manager *

This default implementation reads all items, first, so you might want to adapt this * whenever reading is expensive!

* + * @param filter to filter returned objects * @param comparator to sort the returned objects * @param pageNumber the number of the page to be returned (zero based) * @param pageSize the size of the pages @@ -134,8 +137,8 @@ public interface Manager * page. If the requested page number exceeds the existing pages, an * empty page result is returned. */ - default PageResult getPage(Comparator comparator, int pageNumber, int pageSize) { - return PageResult.createPage(getAll(comparator), pageNumber, pageSize); + default PageResult getPage(Predicate filter, Comparator comparator, int pageNumber, int pageSize) { + return PageResult.createPage(getAll(filter, comparator), pageNumber, pageSize); } } diff --git a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java index f6e91aeced..0c46366a56 100644 --- a/scm-core/src/main/java/sonia/scm/ManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/ManagerDecorator.java @@ -37,6 +37,7 @@ package sonia.scm; import java.io.IOException; import java.util.Collection; import java.util.Comparator; +import java.util.function.Predicate; /** * Basic decorator for manager classes. @@ -104,9 +105,9 @@ public class ManagerDecorator implements Manager { } @Override - public Collection getAll(Comparator comparator) + public Collection getAll(Predicate filter, Comparator comparator) { - return decorated.getAll(comparator); + return decorated.getAll(filter, comparator); } @Override diff --git a/scm-core/src/main/java/sonia/scm/search/SearchUtil.java b/scm-core/src/main/java/sonia/scm/search/SearchUtil.java index 281e00a517..3d86b113ff 100644 --- a/scm-core/src/main/java/sonia/scm/search/SearchUtil.java +++ b/scm-core/src/main/java/sonia/scm/search/SearchUtil.java @@ -92,16 +92,13 @@ public final class SearchUtil { result = true; - if (Util.isNotEmpty(other)) + for (String o : other) { - for (String o : other) + if ((o == null) ||!o.matches(query)) { - if ((o == null) ||!o.matches(query)) - { - result = false; + result = false; - break; - } + break; } } } @@ -127,16 +124,13 @@ public final class SearchUtil if (!value.matches(query)) { - if (Util.isNotEmpty(other)) + for (String o : other) { - for (String o : other) + if ((o != null) && o.matches(query)) { - if ((o != null) && o.matches(query)) - { - result = true; + result = true; - break; - } + break; } } } diff --git a/scm-core/src/test/java/sonia/scm/ManagerTest.java b/scm-core/src/test/java/sonia/scm/ManagerTest.java index 06c8eb3ea6..b26fdb3217 100644 --- a/scm-core/src/test/java/sonia/scm/ManagerTest.java +++ b/scm-core/src/test/java/sonia/scm/ManagerTest.java @@ -5,6 +5,7 @@ import org.mockito.Mock; import java.util.Collection; import java.util.Comparator; +import java.util.function.Predicate; import java.util.stream.IntStream; import static java.util.stream.Collectors.toList; @@ -18,21 +19,22 @@ public class ManagerTest { @Mock private Comparator comparator; + private Predicate predicate = x -> true; @Test(expected = IllegalArgumentException.class) public void validatesPageNumber() { - manager.getPage(comparator, -1, 5); + manager.getPage(predicate, comparator, -1, 5); } @Test(expected = IllegalArgumentException.class) public void validatesPageSize() { - manager.getPage(comparator, 2, 0); + manager.getPage(predicate, comparator, 2, 0); } @Test public void getsNoPage() { givenItemCount = 0; - PageResult singlePage = manager.getPage(comparator, 0, 5); + PageResult singlePage = manager.getPage(predicate, comparator, 0, 5); assertEquals(0, singlePage.getEntities().size()); assertEquals(givenItemCount, singlePage.getOverallCount()); } @@ -40,7 +42,7 @@ public class ManagerTest { @Test public void getsSinglePageWithoutEnoughItems() { givenItemCount = 3; - PageResult singlePage = manager.getPage(comparator, 0, 4); + PageResult singlePage = manager.getPage(predicate, comparator, 0, 4); assertEquals(3, singlePage.getEntities().size() ); assertEquals(givenItemCount, singlePage.getOverallCount()); } @@ -48,7 +50,7 @@ public class ManagerTest { @Test public void getsSinglePageWithExactCountOfItems() { givenItemCount = 3; - PageResult singlePage = manager.getPage(comparator, 0, 3); + PageResult singlePage = manager.getPage(predicate, comparator, 0, 3); assertEquals(3, singlePage.getEntities().size() ); assertEquals(givenItemCount, singlePage.getOverallCount()); } @@ -56,11 +58,11 @@ public class ManagerTest { @Test public void getsTwoPages() { givenItemCount = 3; - PageResult page1 = manager.getPage(comparator, 0, 2); + PageResult page1 = manager.getPage(predicate, comparator, 0, 2); assertEquals(2, page1.getEntities().size()); assertEquals(givenItemCount, page1.getOverallCount()); - PageResult page2 = manager.getPage(comparator, 1, 2); + PageResult page2 = manager.getPage(predicate, comparator, 1, 2); assertEquals(1, page2.getEntities().size()); assertEquals(givenItemCount, page2.getOverallCount()); } @@ -79,7 +81,7 @@ public class ManagerTest { } @Override - public Collection getAll(Comparator comparator) { return getAll(); } + public Collection getAll(Predicate filter, Comparator comparator) { return getAll(); } @Override public Collection getAll(int start, int limit) { return null; } diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json index 3b66c4f2c1..1e37876316 100644 --- a/scm-plugins/scm-git-plugin/package.json +++ b/scm-plugins/scm-git-plugin/package.json @@ -12,6 +12,6 @@ "@scm-manager/ui-extensions": "^0.1.2" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.26" + "@scm-manager/ui-bundler": "^0.0.28" } } diff --git a/scm-plugins/scm-git-plugin/yarn.lock b/scm-plugins/scm-git-plugin/yarn.lock index a45145d157..13b567b95c 100644 --- a/scm-plugins/scm-git-plugin/yarn.lock +++ b/scm-plugins/scm-git-plugin/yarn.lock @@ -707,9 +707,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.26": - version "0.0.26" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json index d723751912..6bf1b18a07 100644 --- a/scm-plugins/scm-hg-plugin/package.json +++ b/scm-plugins/scm-hg-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.1.2" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.26" + "@scm-manager/ui-bundler": "^0.0.28" } } diff --git a/scm-plugins/scm-hg-plugin/yarn.lock b/scm-plugins/scm-hg-plugin/yarn.lock index c1e19efede..ce15b701db 100644 --- a/scm-plugins/scm-hg-plugin/yarn.lock +++ b/scm-plugins/scm-hg-plugin/yarn.lock @@ -641,9 +641,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.26": - version "0.0.26" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json index 10439dba66..e933145c67 100644 --- a/scm-plugins/scm-svn-plugin/package.json +++ b/scm-plugins/scm-svn-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.1.2" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.26" + "@scm-manager/ui-bundler": "^0.0.28" } } diff --git a/scm-plugins/scm-svn-plugin/yarn.lock b/scm-plugins/scm-svn-plugin/yarn.lock index c1e19efede..ce15b701db 100644 --- a/scm-plugins/scm-svn-plugin/yarn.lock +++ b/scm-plugins/scm-svn-plugin/yarn.lock @@ -641,9 +641,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.26": - version "0.0.26" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-ui-components/packages/ui-components/package.json b/scm-ui-components/packages/ui-components/package.json index 991142e338..8b02d2e732 100644 --- a/scm-ui-components/packages/ui-components/package.json +++ b/scm-ui-components/packages/ui-components/package.json @@ -14,7 +14,7 @@ "eslint-fix": "eslint src --fix" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.26", + "@scm-manager/ui-bundler": "^0.0.28", "create-index": "^2.3.0", "enzyme": "^3.5.0", "enzyme-adapter-react-16": "^1.3.1", @@ -30,6 +30,7 @@ "@scm-manager/ui-types": "2.0.0-SNAPSHOT", "classnames": "^2.2.6", "moment": "^2.22.2", + "query-string": "5", "react": "^16.8.6", "react-dom": "^16.8.6", "react-diff-view": "^1.8.1", diff --git a/scm-ui-components/packages/ui-components/src/LinkPaginator.js b/scm-ui-components/packages/ui-components/src/LinkPaginator.js index d09306e04c..a662272b11 100644 --- a/scm-ui-components/packages/ui-components/src/LinkPaginator.js +++ b/scm-ui-components/packages/ui-components/src/LinkPaginator.js @@ -1,18 +1,26 @@ //@flow import React from "react"; -import {translate} from "react-i18next"; -import type {PagedCollection} from "@scm-manager/ui-types"; -import {Button} from "./buttons"; +import { translate } from "react-i18next"; +import type { PagedCollection } from "@scm-manager/ui-types"; +import { Button } from "./buttons"; type Props = { collection: PagedCollection, page: number, + filter?: string, // context props t: string => string }; class LinkPaginator extends React.Component { + addFilterToLink(link: string) { + const { filter } = this.props; + if (filter) { + return `${link}?q=${filter}`; + } + return link; + } renderFirstButton() { return ( @@ -20,7 +28,7 @@ class LinkPaginator extends React.Component { className={"pagination-link"} label={"1"} disabled={false} - link={"1"} + link={this.addFilterToLink("1")} /> ); } @@ -34,7 +42,7 @@ class LinkPaginator extends React.Component { className={className} label={label ? label : previousPage.toString()} disabled={!this.hasLink("prev")} - link={`${previousPage}`} + link={this.addFilterToLink(`${previousPage}`)} /> ); } @@ -52,7 +60,7 @@ class LinkPaginator extends React.Component { className={className} label={label ? label : nextPage.toString()} disabled={!this.hasLink("next")} - link={`${nextPage}`} + link={this.addFilterToLink(`${nextPage}`)} /> ); } @@ -64,7 +72,7 @@ class LinkPaginator extends React.Component { className={"pagination-link"} label={`${collection.pageTotal}`} disabled={false} - link={`${collection.pageTotal}`} + link={this.addFilterToLink(`${collection.pageTotal}`)} /> ); } @@ -115,18 +123,25 @@ class LinkPaginator extends React.Component { } render() { - const { t } = this.props; - return ( - - ); + const { collection, t } = this.props; + + if(collection) { + return ( + + ); + } + return null; } } diff --git a/scm-ui-components/packages/ui-components/src/OverviewPageActions.js b/scm-ui-components/packages/ui-components/src/OverviewPageActions.js new file mode 100644 index 0000000000..84d0713752 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/OverviewPageActions.js @@ -0,0 +1,58 @@ +// @flow +import React from "react"; +import type { History } from "history"; +import { withRouter } from "react-router-dom"; +import classNames from "classnames"; +import injectSheet from "react-jss"; +import { FilterInput } from "./forms"; +import { Button, urls } from "./index"; + +type Props = { + showCreateButton: boolean, + link: string, + label?: string, + + // context props + classes: Object, + history: History, + location: any +}; + +const styles = { + button: { + float: "right", + marginTop: "1.25rem", + marginLeft: "1.25rem" + } +}; + +class OverviewPageActions extends React.Component { + render() { + const { history, location, link } = this.props; + return ( + <> + { + history.push(`/${link}/?q=${filter}`); + }} + /> + {this.renderCreateButton()} + + ); + } + + renderCreateButton() { + const { showCreateButton, classes, link, label } = this.props; + if (showCreateButton) { + return ( +
+
+ ); + } + return null; + } +} + +export default injectSheet(styles)(withRouter(OverviewPageActions)); diff --git a/scm-ui-components/packages/ui-components/src/forms/FilterInput.js b/scm-ui-components/packages/ui-components/src/forms/FilterInput.js new file mode 100644 index 0000000000..dc97b4856e --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/forms/FilterInput.js @@ -0,0 +1,69 @@ +//@flow +import React from "react"; +import injectSheet from "react-jss"; +import classNames from "classnames"; +import { translate } from "react-i18next"; + +type Props = { + filter: string => void, + value?: string, + + // context props + classes: Object, + t: string => string +}; + +type State = { + value: string +}; + +const styles = { + inputField: { + float: "right", + marginTop: "1.25rem" + }, + inputHeight: { + height: "2.5rem" + } +}; + +class FilterInput extends React.Component { + constructor(props) { + super(props); + this.state = { value: this.props.value ? this.props.value : "" }; + } + + handleChange = event => { + this.setState({ value: event.target.value }); + }; + + handleSubmit = event => { + this.props.filter(this.state.value); + event.preventDefault(); + }; + + render() { + const { classes, t } = this.props; + return ( +
+
+ + + + +
+ + ); + } +} + +export default injectSheet(styles)(translate("commons")(FilterInput)); diff --git a/scm-ui-components/packages/ui-components/src/forms/index.js b/scm-ui-components/packages/ui-components/src/forms/index.js index ef4d7a1ae4..aed38d44bc 100644 --- a/scm-ui-components/packages/ui-components/src/forms/index.js +++ b/scm-ui-components/packages/ui-components/src/forms/index.js @@ -5,6 +5,7 @@ export { default as AutocompleteAddEntryToTableField } from "./AutocompleteAddEn export { default as MemberNameTable } from "./MemberNameTable.js"; export { default as Checkbox } from "./Checkbox.js"; export { default as Radio } from "./Radio.js"; +export { default as FilterInput } from "./FilterInput.js"; export { default as InputField } from "./InputField.js"; export { default as Select } from "./Select.js"; export { default as Textarea } from "./Textarea.js"; diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index 2268f96e05..954b0b0955 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -22,12 +22,14 @@ export { default as ProtectedRoute } from "./ProtectedRoute.js"; export { default as Help } from "./Help"; export { default as HelpIcon } from "./HelpIcon"; export { default as Tooltip } from "./Tooltip"; +// TODO do we need this? getPageFromMatch is already exported by urls export { getPageFromMatch } from "./urls"; export { default as Autocomplete} from "./Autocomplete"; export { default as BranchSelector } from "./BranchSelector"; export { default as MarkdownView } from "./MarkdownView"; export { default as SyntaxHighlighter } from "./SyntaxHighlighter"; export { default as ErrorBoundary } from "./ErrorBoundary"; +export { default as OverviewPageActions } from "./OverviewPageActions.js"; export { apiClient } from "./apiclient.js"; export * from "./errors"; diff --git a/scm-ui-components/packages/ui-components/src/layout/Page.js b/scm-ui-components/packages/ui-components/src/layout/Page.js index 655b1fe986..ef4363cd8f 100644 --- a/scm-ui-components/packages/ui-components/src/layout/Page.js +++ b/scm-ui-components/packages/ui-components/src/layout/Page.js @@ -1,11 +1,11 @@ //@flow import * as React from "react"; +import injectSheet from "react-jss"; +import classNames from "classnames"; import Loading from "./../Loading"; import ErrorNotification from "./../ErrorNotification"; import Title from "./Title"; import Subtitle from "./Subtitle"; -import injectSheet from "react-jss"; -import classNames from "classnames"; import PageActions from "./PageActions"; import ErrorBoundary from "../ErrorBoundary"; @@ -22,9 +22,9 @@ type Props = { }; const styles = { - spacing: { - marginTop: "1.25rem", - textAlign: "right" + actions: { + display: "flex", + justifyContent: "flex-end" } }; @@ -36,47 +36,45 @@ class Page extends React.Component {
{this.renderPageHeader()} - + {this.renderContent()}
- ); } renderPageHeader() { - const { title, subtitle, children, classes } = this.props; + const { error, title, subtitle, children, classes } = this.props; let pageActions = null; let pageActionsExists = false; React.Children.forEach(children, child => { - if (child && child.type.name === PageActions.name) { - pageActions = ( -
+ if (child && !error) { + if (child.type.name === PageActions.name) + pageActions = (
{child}
-
- ); + ); pageActionsExists = true; } }); let underline = pageActionsExists ? ( -
+
) : null; return ( <>
- - <Subtitle subtitle={subtitle}/> + <Title title={title} /> + <Subtitle subtitle={subtitle} /> </div> {pageActions} </div> @@ -92,13 +90,15 @@ class Page extends React.Component<Props> { return null; } if (loading) { - return <Loading/>; + return <Loading />; } let content = []; React.Children.forEach(children, child => { - if (child && child.type.name !== PageActions.name) { - content.push(child); + if (child) { + if (child.type.name !== PageActions.name) { + content.push(child); + } } }); return content; diff --git a/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js b/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js index 897c63138e..ce0c2d75dc 100644 --- a/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js +++ b/scm-ui-components/packages/ui-components/src/navigation/PrimaryNavigation.js @@ -7,12 +7,11 @@ import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; type Props = { t: string => string, - links: Links, + links: Links }; class PrimaryNavigation extends React.Component<Props> { - - createNavigationAppender = (navigationItems) => { + createNavigationAppender = navigationItems => { const { t, links } = this.props; return (to: string, match: string, label: string, linkName: string) => { @@ -24,8 +23,8 @@ class PrimaryNavigation extends React.Component<Props> { match={match} label={t(label)} key={linkName} - />) - ; + /> + ); navigationItems.push(navigationItem); } }; @@ -63,16 +62,26 @@ class PrimaryNavigation extends React.Component<Props> { <ExtensionPoint name="primary-navigation.first-menu" props={props} /> ); } - append("/repos", "/(repo|repos)", "primary-navigation.repositories", "repositories"); - append("/users", "/(user|users)", "primary-navigation.users", "users"); - append("/groups", "/(group|groups)", "primary-navigation.groups", "groups"); + append( + "/repos/", + "/(repo|repos)", + "primary-navigation.repositories", + "repositories" + ); + append("/users/", "/(user|users)", "primary-navigation.users", "users"); + append( + "/groups/", + "/(group|groups)", + "primary-navigation.groups", + "groups" + ); append("/config", "/config", "primary-navigation.config", "config"); navigationItems.push( <ExtensionPoint name="primary-navigation" renderAll={true} - props={{links: this.props.links}} + props={{ links: this.props.links }} /> ); @@ -86,9 +95,7 @@ class PrimaryNavigation extends React.Component<Props> { return ( <nav className="tabs is-boxed"> - <ul> - {navigationItems} - </ul> + <ul>{navigationItems}</ul> </nav> ); } diff --git a/scm-ui-components/packages/ui-components/src/urls.js b/scm-ui-components/packages/ui-components/src/urls.js index dd8888d7a3..54b1c4294f 100644 --- a/scm-ui-components/packages/ui-components/src/urls.js +++ b/scm-ui-components/packages/ui-components/src/urls.js @@ -1,4 +1,6 @@ // @flow +import queryString from "query-string"; + export const contextPath = window.ctxPath || ""; export function withContextPath(path: string) { @@ -27,3 +29,7 @@ export function getPageFromMatch(match: any) { } return page; } + +export function getQueryStringFromLocation(location: any) { + return location.search ? queryString.parse(location.search).q : undefined; +} diff --git a/scm-ui-components/packages/ui-components/src/urls.test.js b/scm-ui-components/packages/ui-components/src/urls.test.js index 60f27510b8..7eae3a0331 100644 --- a/scm-ui-components/packages/ui-components/src/urls.test.js +++ b/scm-ui-components/packages/ui-components/src/urls.test.js @@ -1,5 +1,5 @@ // @flow -import {concat, getPageFromMatch, withEndingSlash} from "./urls"; +import { concat, getPageFromMatch, getQueryStringFromLocation, withEndingSlash } from "./urls"; describe("tests for withEndingSlash", () => { @@ -47,3 +47,28 @@ describe("tests for getPageFromMatch", () => { expect(getPageFromMatch(match)).toBe(42); }); }); + +describe("tests for getQueryStringFromLocation", () => { + + function createLocation(search: string) { + return { + search + }; + } + + it("should return the query string", () => { + const location = createLocation("?q=abc"); + expect(getQueryStringFromLocation(location)).toBe("abc"); + }); + + it("should return query string from multiple parameters", () => { + const location = createLocation("?x=a&y=b&q=abc&z=c"); + expect(getQueryStringFromLocation(location)).toBe("abc"); + }); + + it("should return undefined if q is not available", () => { + const location = createLocation("?x=a&y=b&z=c"); + expect(getQueryStringFromLocation(location)).toBeUndefined(); + }); + +}); diff --git a/scm-ui-components/packages/ui-components/yarn.lock b/scm-ui-components/packages/ui-components/yarn.lock index bce98a17e4..e0a601e53f 100644 --- a/scm-ui-components/packages/ui-components/yarn.lock +++ b/scm-ui-components/packages/ui-components/yarn.lock @@ -693,9 +693,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.26": - version "0.0.26" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -6530,6 +6530,14 @@ qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" +query-string@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/query-string/-/query-string-5.1.1.tgz#a78c012b71c17e05f2e3fa2319dd330682efb3cb" + dependencies: + decode-uri-component "^0.2.0" + object-assign "^4.1.0" + strict-uri-encode "^1.0.0" + querystring-es3@~0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" @@ -7631,6 +7639,10 @@ stream-throttle@^0.1.3: commander "^2.2.0" limiter "^1.0.5" +strict-uri-encode@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" diff --git a/scm-ui-components/packages/ui-types/package.json b/scm-ui-components/packages/ui-types/package.json index 471faf87be..27c7ff80b6 100644 --- a/scm-ui-components/packages/ui-types/package.json +++ b/scm-ui-components/packages/ui-types/package.json @@ -14,7 +14,7 @@ "check": "flow check" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.26" + "@scm-manager/ui-bundler": "^0.0.28" }, "browserify": { "transform": [ @@ -33,4 +33,4 @@ ] ] } -} \ No newline at end of file +} diff --git a/scm-ui-components/packages/ui-types/yarn.lock b/scm-ui-components/packages/ui-types/yarn.lock index 2d47639005..3a67b6d419 100644 --- a/scm-ui-components/packages/ui-types/yarn.lock +++ b/scm-ui-components/packages/ui-types/yarn.lock @@ -707,9 +707,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.26": - version "0.0.26" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.26.tgz#4676a7079b781b33fa1989c6643205c3559b1f66" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-ui/package.json b/scm-ui/package.json index 3c730540ae..d157b2a772 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -54,7 +54,7 @@ "pre-commit": "jest && flow && eslint src" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.27", + "@scm-manager/ui-bundler": "^0.0.28", "concat": "^1.0.3", "copyfiles": "^2.0.0", "enzyme": "^3.3.0", diff --git a/scm-ui/public/locales/de/commons.json b/scm-ui/public/locales/de/commons.json index 51a493fc40..b9cc93b1ed 100644 --- a/scm-ui/public/locales/de/commons.json +++ b/scm-ui/public/locales/de/commons.json @@ -39,6 +39,7 @@ "groups": "Gruppen", "config": "Einstellungen" }, + "filterEntries": "Einträge filtern", "paginator": { "next": "Weiter", "previous": "Zurück" diff --git a/scm-ui/public/locales/de/users.json b/scm-ui/public/locales/de/users.json index 05b5cf142f..37416c1e4f 100644 --- a/scm-ui/public/locales/de/users.json +++ b/scm-ui/public/locales/de/users.json @@ -39,7 +39,7 @@ "setPermissionsNavLink": "Berechtigungen" } }, - "addUser": { + "createUser": { "title": "Benutzer erstellen", "subtitle": "Erstellen eines neuen Benutzers" }, diff --git a/scm-ui/public/locales/en/commons.json b/scm-ui/public/locales/en/commons.json index cc4edde82c..b5af3e9ef9 100644 --- a/scm-ui/public/locales/en/commons.json +++ b/scm-ui/public/locales/en/commons.json @@ -39,6 +39,7 @@ "groups": "Groups", "config": "Configuration" }, + "filterEntries": "filter entries", "paginator": { "next": "Next", "previous": "Previous" diff --git a/scm-ui/public/locales/en/users.json b/scm-ui/public/locales/en/users.json index e6fd822ead..d188f6221b 100644 --- a/scm-ui/public/locales/en/users.json +++ b/scm-ui/public/locales/en/users.json @@ -39,7 +39,7 @@ "setPermissionsNavLink": "Permissions" } }, - "addUser": { + "createUser": { "title": "Create User", "subtitle": "Create a new user" }, diff --git a/scm-ui/src/containers/Main.js b/scm-ui/src/containers/Main.js index 570455d18d..42a6c54590 100644 --- a/scm-ui/src/containers/Main.js +++ b/scm-ui/src/containers/Main.js @@ -2,24 +2,24 @@ import React from "react"; import { Redirect, Route, Switch, withRouter } from "react-router-dom"; -import type {Links} from "@scm-manager/ui-types"; +import type { Links } from "@scm-manager/ui-types"; import Overview from "../repos/containers/Overview"; import Users from "../users/containers/Users"; import Login from "../containers/Login"; import Logout from "../containers/Logout"; -import {ProtectedRoute} from "@scm-manager/ui-components"; -import {binder, ExtensionPoint } from "@scm-manager/ui-extensions"; +import { ProtectedRoute } from "@scm-manager/ui-components"; +import { binder, ExtensionPoint } from "@scm-manager/ui-extensions"; -import AddUser from "../users/containers/AddUser"; +import CreateUser from "../users/containers/CreateUser"; import SingleUser from "../users/containers/SingleUser"; import RepositoryRoot from "../repos/containers/RepositoryRoot"; import Create from "../repos/containers/Create"; import Groups from "../groups/containers/Groups"; import SingleGroup from "../groups/containers/SingleGroup"; -import AddGroup from "../groups/containers/AddGroup"; +import CreateGroup from "../groups/containers/CreateGroup"; import Config from "../config/containers/Config"; import Profile from "./Profile"; @@ -33,14 +33,14 @@ class Main extends React.Component<Props> { render() { const { authenticated, links } = this.props; const redirectUrlFactory = binder.getExtension("main.redirect", this.props); - let url ="/repos"; - if (redirectUrlFactory){ + let url = "/repos"; + if (redirectUrlFactory) { url = redirectUrlFactory(this.props); } return ( <div className="main"> <Switch> - <Redirect exact from="/" to={url}/> + <Redirect exact from="/" to={url} /> <Route exact path="/login" component={Login} /> <Route path="/logout" component={Logout} /> <ProtectedRoute @@ -74,8 +74,8 @@ class Main extends React.Component<Props> { /> <ProtectedRoute authenticated={authenticated} - path="/users/add" - component={AddUser} + path="/users/create" + component={CreateUser} /> <ProtectedRoute exact @@ -102,8 +102,8 @@ class Main extends React.Component<Props> { /> <ProtectedRoute authenticated={authenticated} - path="/groups/add" - component={AddGroup} + path="/groups/create" + component={CreateGroup} /> <ProtectedRoute exact @@ -125,7 +125,7 @@ class Main extends React.Component<Props> { <ExtensionPoint name="main.route" renderAll={true} - props={{authenticated, links}} + props={{ authenticated, links }} /> </Switch> </div> diff --git a/scm-ui/src/groups/components/GroupForm.js b/scm-ui/src/groups/components/GroupForm.js index d3b6799860..baa68de8ef 100644 --- a/scm-ui/src/groups/components/GroupForm.js +++ b/scm-ui/src/groups/components/GroupForm.js @@ -40,7 +40,8 @@ class GroupForm extends React.Component<Props, State> { }, _links: {}, members: [], - type: "" + type: "", + external: false }, nameValidationError: false }; diff --git a/scm-ui/src/groups/components/buttons/CreateGroupButton.js b/scm-ui/src/groups/components/buttons/CreateGroupButton.js deleted file mode 100644 index 73e0eddd63..0000000000 --- a/scm-ui/src/groups/components/buttons/CreateGroupButton.js +++ /dev/null @@ -1,19 +0,0 @@ -//@flow -import React from "react"; -import { translate } from "react-i18next"; -import { CreateButton } from "@scm-manager/ui-components"; - -type Props = { - t: string => string -}; - -class CreateGroupButton extends React.Component<Props> { - render() { - const { t } = this.props; - return ( - <CreateButton label={t("create-group-button.label")} link="/groups/add" /> - ); - } -} - -export default translate("groups")(CreateGroupButton); diff --git a/scm-ui/src/groups/containers/AddGroup.js b/scm-ui/src/groups/containers/CreateGroup.js similarity index 96% rename from scm-ui/src/groups/containers/AddGroup.js rename to scm-ui/src/groups/containers/CreateGroup.js index 69c1171ea9..40c2a6cb8a 100644 --- a/scm-ui/src/groups/containers/AddGroup.js +++ b/scm-ui/src/groups/containers/CreateGroup.js @@ -31,7 +31,7 @@ type Props = { type State = {}; -class AddGroup extends React.Component<Props, State> { +class CreateGroup extends React.Component<Props, State> { componentDidMount() { this.props.resetForm(); } @@ -104,4 +104,4 @@ const mapStateToProps = state => { export default connect( mapStateToProps, mapDispatchToProps -)(translate("groups")(AddGroup)); +)(translate("groups")(CreateGroup)); diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index d54f4f5be4..60672bf830 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -2,27 +2,26 @@ import React from "react"; import { connect } from "react-redux"; import { translate } from "react-i18next"; -import type { Group, PagedCollection } from "@scm-manager/ui-types"; import type { History } from "history"; -import { - Page, - PageActions, - Button, - Notification, - Paginator -} from "@scm-manager/ui-components"; -import { GroupTable } from "./../components/table"; -import CreateGroupButton from "../components/buttons/CreateGroupButton"; - +import type { Group, PagedCollection } from "@scm-manager/ui-types"; import { fetchGroupsByPage, - fetchGroupsByLink, getGroupsFromState, isFetchGroupsPending, getFetchGroupsFailure, isPermittedToCreateGroups, selectListAsCollection } from "../modules/groups"; +import { + Page, + PageActions, + OverviewPageActions, + Notification, + LinkPaginator, + urls, + CreateButton +} from "@scm-manager/ui-components"; +import { GroupTable } from "./../components/table"; import { getGroupsLink } from "../../modules/indexResource"; type Props = { @@ -37,37 +36,45 @@ type Props = { // context objects t: string => string, history: History, + location: any, // dispatch functions - fetchGroupsByPage: (link: string, page: number) => void, - fetchGroupsByLink: (link: string) => void + fetchGroupsByPage: (link: string, page: number, filter?: string) => void }; class Groups extends React.Component<Props> { componentDidMount() { - this.props.fetchGroupsByPage(this.props.groupLink, this.props.page); + const { fetchGroupsByPage, groupLink, page, location } = this.props; + fetchGroupsByPage( + groupLink, + page, + urls.getQueryStringFromLocation(location) + ); } - onPageChange = (link: string) => { - this.props.fetchGroupsByLink(link); - }; - - /** - * reflect page transitions in the uri - */ componentDidUpdate = (prevProps: Props) => { - const { page, list } = this.props; - if (list.page >= 0) { - // backend starts paging by 0 + const { + loading, + list, + page, + groupLink, + location, + fetchGroupsByPage + } = this.props; + if (list && page && !loading) { const statePage: number = list.page + 1; - if (page !== statePage) { - this.props.history.push(`/groups/${statePage}`); + if (page !== statePage || prevProps.location.search !== location.search) { + fetchGroupsByPage( + groupLink, + page, + urls.getQueryStringFromLocation(location) + ); } } }; render() { - const { groups, loading, error, t } = this.props; + const { groups, loading, error, canAddGroups, t } = this.props; return ( <Page title={t("groups.title")} @@ -77,74 +84,56 @@ class Groups extends React.Component<Props> { > {this.renderGroupTable()} {this.renderCreateButton()} - {this.renderPageActionCreateButton()} + <PageActions> + <OverviewPageActions + showCreateButton={canAddGroups} + link="groups" + label={t("create-group-button.label")} + /> + </PageActions> </Page> ); } renderGroupTable() { - const { groups, t } = this.props; + const { groups, list, page, location, t } = this.props; if (groups && groups.length > 0) { return ( <> <GroupTable groups={groups} /> - {this.renderPaginator()} + <LinkPaginator + collection={list} + page={page} + filter={urls.getQueryStringFromLocation(location)} + /> </> ); } return <Notification type="info">{t("groups.noGroups")}</Notification>; } - renderPaginator() { - const { list } = this.props; - if (list) { - return <Paginator collection={list} onPageChange={this.onPageChange} />; - } - return null; - } - renderCreateButton() { - if (this.props.canAddGroups) { - return <CreateGroupButton />; - } - return null; - } - - renderPageActionCreateButton() { - if (this.props.canAddGroups) { + const { canAddGroups, t } = this.props; + if (canAddGroups) { return ( - <PageActions> - <Button - label={this.props.t("create-group-button.label")} - link="/groups/add" - color="primary" - /> - </PageActions> + <CreateButton + label={t("create-group-button.label")} + link="/groups/create" + /> ); } return null; } } -const getPageFromProps = props => { - let page = props.match.params.page; - if (page) { - page = parseInt(page, 10); - } else { - page = 1; - } - return page; -}; - const mapStateToProps = (state, ownProps) => { + const { match } = ownProps; const groups = getGroupsFromState(state); const loading = isFetchGroupsPending(state); const error = getFetchGroupsFailure(state); - - const page = getPageFromProps(ownProps); + const page = urls.getPageFromMatch(match); const canAddGroups = isPermittedToCreateGroups(state); const list = selectListAsCollection(state); - const groupLink = getGroupsLink(state); return { @@ -160,11 +149,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchGroupsByPage: (link: string, page: number) => { - dispatch(fetchGroupsByPage(link, page)); - }, - fetchGroupsByLink: (link: string) => { - dispatch(fetchGroupsByLink(link)); + fetchGroupsByPage: (link: string, page: number, filter?: string) => { + dispatch(fetchGroupsByPage(link, page, filter)); } }; }; diff --git a/scm-ui/src/groups/modules/groups.js b/scm-ui/src/groups/modules/groups.js index bbaccf6c4a..cb3c24aa0f 100644 --- a/scm-ui/src/groups/modules/groups.js +++ b/scm-ui/src/groups/modules/groups.js @@ -40,9 +40,14 @@ export function fetchGroups(link: string) { return fetchGroupsByLink(link); } -export function fetchGroupsByPage(link: string, page: number) { +export function fetchGroupsByPage(link: string, page: number, filter?: string) { // backend start counting by 0 - return fetchGroupsByLink(link + "?page=" + (page - 1)); + if (filter) { + return fetchGroupsByLink( + `${link}?page=${page - 1}&q=${decodeURIComponent(filter)}` + ); + } + return fetchGroupsByLink(`${link}?page=${page - 1}`); } export function fetchGroupsByLink(link: string) { diff --git a/scm-ui/src/modules/auth.js b/scm-ui/src/modules/auth.js index 73ba013fa1..fdc3c83c7f 100644 --- a/scm-ui/src/modules/auth.js +++ b/scm-ui/src/modules/auth.js @@ -97,14 +97,14 @@ export const logoutPending = () => { export const logoutSuccess = () => { return { - type: LOGOUT_SUCCESS, + type: LOGOUT_SUCCESS }; }; export const redirectAfterLogout = () => { return { type: LOGOUT_REDIRECT - } + }; }; export const logoutFailure = (error: Error) => { @@ -277,4 +277,3 @@ export const getLogoutFailure = (state: Object) => { export const isRedirecting = (state: Object) => { return !!stateAuth(state).redirecting; }; - diff --git a/scm-ui/src/repos/branches/modules/branches.js b/scm-ui/src/repos/branches/modules/branches.js index 0efabcc90c..40228eac91 100644 --- a/scm-ui/src/repos/branches/modules/branches.js +++ b/scm-ui/src/repos/branches/modules/branches.js @@ -15,7 +15,7 @@ import type { import { isPending } from "../../../modules/pending"; import { getFailure } from "../../../modules/failure"; -import memoizeOne from 'memoize-one'; +import memoizeOne from "memoize-one"; export const FETCH_BRANCHES = "scm/repos/FETCH_BRANCHES"; export const FETCH_BRANCHES_PENDING = `${FETCH_BRANCHES}_${PENDING_SUFFIX}`; @@ -111,9 +111,7 @@ export function createBranch( // Selectors function collectBranches(repoState) { - return repoState.list._embedded.branches.map( - name => repoState.byName[name] - ); + return repoState.list._embedded.branches.map(name => repoState.byName[name]); } const memoizedBranchCollector = memoizeOne(collectBranches); @@ -127,7 +125,12 @@ export function getBranches(state: Object, repository: Repository) { export function getBranchCreateLink(state: Object, repository: Repository) { const repoState = getRepoState(state, repository); - if (repoState && repoState.list && repoState.list._links && repoState.list._links.create) { + if ( + repoState && + repoState.list && + repoState.list._links && + repoState.list._links.create + ) { return repoState.list._links.create.href; } } diff --git a/scm-ui/src/repos/containers/Overview.js b/scm-ui/src/repos/containers/Overview.js index b8cffae191..9dcaf99b72 100644 --- a/scm-ui/src/repos/containers/Overview.js +++ b/scm-ui/src/repos/containers/Overview.js @@ -1,71 +1,79 @@ // @flow import React from "react"; - -import type { RepositoryCollection } from "@scm-manager/ui-types"; - import { connect } from "react-redux"; +import { translate } from "react-i18next"; +import type { History } from "history"; +import { withRouter } from "react-router-dom"; +import type { RepositoryCollection } from "@scm-manager/ui-types"; import { - fetchRepos, - fetchReposByLink, fetchReposByPage, getFetchReposFailure, getRepositoryCollection, isAbleToCreateRepos, isFetchReposPending } from "../modules/repos"; -import { translate } from "react-i18next"; import { Page, PageActions, - Button, + OverviewPageActions, CreateButton, Notification, - Paginator + LinkPaginator, + urls } from "@scm-manager/ui-components"; import RepositoryList from "../components/list"; -import { withRouter } from "react-router-dom"; -import type { History } from "history"; import { getRepositoriesLink } from "../../modules/indexResource"; type Props = { - page: number, - collection: RepositoryCollection, loading: boolean, error: Error, showCreateButton: boolean, + collection: RepositoryCollection, + page: number, reposLink: string, - // dispatched functions - fetchRepos: string => void, - fetchReposByPage: (string, number) => void, - fetchReposByLink: string => void, - // context props t: string => string, - history: History + history: History, + location: any, + + // dispatched functions + fetchReposByPage: (link: string, page: number, filter?: string) => void }; class Overview extends React.Component<Props> { componentDidMount() { - this.props.fetchReposByPage(this.props.reposLink, this.props.page); + const { fetchReposByPage, reposLink, page, location } = this.props; + fetchReposByPage( + reposLink, + page, + urls.getQueryStringFromLocation(location) + ); } - /** - * reflect page transitions in the uri - */ - componentDidUpdate() { - const { page, collection } = this.props; - if (collection) { - // backend starts paging by 0 + componentDidUpdate = (prevProps: Props) => { + const { + loading, + collection, + page, + reposLink, + location, + fetchReposByPage + } = this.props; + if (collection && page && !loading) { const statePage: number = collection.page + 1; - if (page !== statePage) { - this.props.history.push(`/repos/${statePage}`); + if (page !== statePage || prevProps.location.search !== location.search) { + fetchReposByPage( + reposLink, + page, + urls.getQueryStringFromLocation(location) + ); } } - } + }; render() { - const { error, loading, t } = this.props; + const { error, loading, showCreateButton, t } = this.props; return ( <Page title={t("overview.title")} @@ -74,19 +82,29 @@ class Overview extends React.Component<Props> { error={error} > {this.renderOverview()} - {this.renderPageActionCreateButton()} + <PageActions> + <OverviewPageActions + showCreateButton={showCreateButton} + link="repos" + label={t("overview.createButton")} + /> + </PageActions> </Page> ); } renderRepositoryList() { - const { collection, fetchReposByLink, t } = this.props; + const { collection, page, location, t } = this.props; if (collection._embedded && collection._embedded.repositories.length > 0) { return ( <> <RepositoryList repositories={collection._embedded.repositories} /> - <Paginator collection={collection} onPageChange={fetchReposByLink} /> + <LinkPaginator + collection={collection} + page={page} + filter={urls.getQueryStringFromLocation(location)} + /> </> ); } @@ -99,10 +117,10 @@ class Overview extends React.Component<Props> { const { collection } = this.props; if (collection) { return ( - <div> + <> {this.renderRepositoryList()} {this.renderCreateButton()} - </div> + </> ); } return null; @@ -117,61 +135,30 @@ class Overview extends React.Component<Props> { } return null; } - - renderPageActionCreateButton() { - const { showCreateButton, t } = this.props; - if (showCreateButton) { - return ( - <PageActions> - <Button - label={t("overview.createButton")} - link="/repos/create" - color="primary" - /> - </PageActions> - ); - } - return null; - } } -const getPageFromProps = props => { - let page = props.match.params.page; - if (page) { - page = parseInt(page, 10); - } else { - page = 1; - } - return page; -}; - const mapStateToProps = (state, ownProps) => { - const page = getPageFromProps(ownProps); + const { match } = ownProps; const collection = getRepositoryCollection(state); const loading = isFetchReposPending(state); const error = getFetchReposFailure(state); + const page = urls.getPageFromMatch(match); const showCreateButton = isAbleToCreateRepos(state); const reposLink = getRepositoriesLink(state); return { - reposLink, - page, collection, loading, error, - showCreateButton + page, + showCreateButton, + reposLink }; }; const mapDispatchToProps = dispatch => { return { - fetchRepos: (link: string) => { - dispatch(fetchRepos(link)); - }, - fetchReposByPage: (link: string, page: number) => { - dispatch(fetchReposByPage(link, page)); - }, - fetchReposByLink: (link: string) => { - dispatch(fetchReposByLink(link)); + fetchReposByPage: (link: string, page: number, filter?: string) => { + dispatch(fetchReposByPage(link, page, filter)); } }; }; diff --git a/scm-ui/src/repos/modules/repos.js b/scm-ui/src/repos/modules/repos.js index fa89dc42a6..cd48f89fea 100644 --- a/scm-ui/src/repos/modules/repos.js +++ b/scm-ui/src/repos/modules/repos.js @@ -46,7 +46,12 @@ export function fetchRepos(link: string) { return fetchReposByLink(link); } -export function fetchReposByPage(link: string, page: number) { +export function fetchReposByPage(link: string, page: number, filter?: string) { + if (filter) { + return fetchReposByLink( + `${link}?page=${page - 1}&q=${decodeURIComponent(filter)}` + ); + } return fetchReposByLink(`${link}?page=${page - 1}`); } diff --git a/scm-ui/src/users/containers/AddUser.js b/scm-ui/src/users/containers/CreateUser.js similarity index 91% rename from scm-ui/src/users/containers/AddUser.js rename to scm-ui/src/users/containers/CreateUser.js index 069df04187..dd2a0dac05 100644 --- a/scm-ui/src/users/containers/AddUser.js +++ b/scm-ui/src/users/containers/CreateUser.js @@ -28,7 +28,7 @@ type Props = { history: History }; -class AddUser extends React.Component<Props> { +class CreateUser extends React.Component<Props> { componentDidMount() { this.props.resetForm(); } @@ -49,8 +49,8 @@ class AddUser extends React.Component<Props> { return ( <Page - title={t("addUser.title")} - subtitle={t("addUser.subtitle")} + title={t("createUser.title")} + subtitle={t("createUser.subtitle")} error={error} showContentOnError={true} > @@ -88,4 +88,4 @@ const mapStateToProps = (state, ownProps) => { export default connect( mapStateToProps, mapDispatchToProps -)(translate("users")(AddUser)); +)(translate("users")(CreateUser)); diff --git a/scm-ui/src/users/containers/Users.js b/scm-ui/src/users/containers/Users.js index 66a4102fa8..7fd279bc45 100644 --- a/scm-ui/src/users/containers/Users.js +++ b/scm-ui/src/users/containers/Users.js @@ -1,29 +1,27 @@ // @flow import React from "react"; -import type { History } from "history"; import { connect } from "react-redux"; import { translate } from "react-i18next"; - +import type { History } from "history"; +import type { User, PagedCollection } from "@scm-manager/ui-types"; import { fetchUsersByPage, - fetchUsersByLink, getUsersFromState, selectListAsCollection, isPermittedToCreateUsers, isFetchUsersPending, getFetchUsersFailure } from "../modules/users"; - import { Page, PageActions, - Button, - CreateButton, - Paginator, - Notification + OverviewPageActions, + Notification, + LinkPaginator, + urls, + CreateButton } from "@scm-manager/ui-components"; import { UserTable } from "./../components/table"; -import type { User, PagedCollection } from "@scm-manager/ui-types"; import { getUsersLink } from "../../modules/indexResource"; type Props = { @@ -38,37 +36,45 @@ type Props = { // context objects t: string => string, history: History, + location: any, // dispatch functions - fetchUsersByPage: (link: string, page: number) => void, - fetchUsersByLink: (link: string) => void + fetchUsersByPage: (link: string, page: number, filter?: string) => void }; class Users extends React.Component<Props> { componentDidMount() { - this.props.fetchUsersByPage(this.props.usersLink, this.props.page); + const { fetchUsersByPage, usersLink, page, location } = this.props; + fetchUsersByPage( + usersLink, + page, + urls.getQueryStringFromLocation(location) + ); } - onPageChange = (link: string) => { - this.props.fetchUsersByLink(link); - }; - - /** - * reflect page transitions in the uri - */ - componentDidUpdate() { - const { page, list } = this.props; - if (list && (list.page || list.page === 0)) { - // backend starts paging by 0 + componentDidUpdate = (prevProps: Props) => { + const { + loading, + list, + page, + usersLink, + location, + fetchUsersByPage + } = this.props; + if (list && page && !loading) { const statePage: number = list.page + 1; - if (page !== statePage) { - this.props.history.push(`/users/${statePage}`); + if (page !== statePage || prevProps.location.search !== location.search) { + fetchUsersByPage( + usersLink, + page, + urls.getQueryStringFromLocation(location) + ); } } - } + }; render() { - const { users, loading, error, t } = this.props; + const { users, loading, error, canAddUsers, t } = this.props; return ( <Page title={t("users.title")} @@ -78,79 +84,54 @@ class Users extends React.Component<Props> { > {this.renderUserTable()} {this.renderCreateButton()} - {this.renderPageActionCreateButton()} + <PageActions> + <OverviewPageActions + showCreateButton={canAddUsers} + link="users" + label={t("users.createButton")} + /> + </PageActions> </Page> ); } renderUserTable() { - const { users, t } = this.props; + const { users, list, page, location, t } = this.props; if (users && users.length > 0) { return ( <> <UserTable users={users} /> - {this.renderPaginator()} + <LinkPaginator + collection={list} + page={page} + filter={urls.getQueryStringFromLocation(location)} + /> </> ); } return <Notification type="info">{t("users.noUsers")}</Notification>; } - renderPaginator() { - const { list } = this.props; - if (list) { - return <Paginator collection={list} onPageChange={this.onPageChange} />; + renderCreateButton() { + const { canAddUsers, t } = this.props; + if (canAddUsers) { + return ( + <CreateButton label={t("users.createButton")} link="/users/create" /> + ); } return null; } - - renderCreateButton() { - const { t } = this.props; - if (this.props.canAddUsers) { - return <CreateButton label={t("users.createButton")} link="/users/add" />; - } else { - return; - } - } - - renderPageActionCreateButton() { - const { t } = this.props; - if (this.props.canAddUsers) { - return ( - <PageActions> - <Button - label={t("users.createButton")} - link="/users/add" - color="primary" - /> - </PageActions> - ); - } else { - return; - } - } } -const getPageFromProps = props => { - let page = props.match.params.page; - if (page) { - page = parseInt(page, 10); - } else { - page = 1; - } - return page; -}; - const mapStateToProps = (state, ownProps) => { + const { match } = ownProps; const users = getUsersFromState(state); const loading = isFetchUsersPending(state); const error = getFetchUsersFailure(state); - - const usersLink = getUsersLink(state); - - const page = getPageFromProps(ownProps); + const page = urls.getPageFromMatch(match); const canAddUsers = isPermittedToCreateUsers(state); const list = selectListAsCollection(state); + const usersLink = getUsersLink(state); return { users, @@ -165,11 +146,8 @@ const mapStateToProps = (state, ownProps) => { const mapDispatchToProps = dispatch => { return { - fetchUsersByPage: (link: string, page: number) => { - dispatch(fetchUsersByPage(link, page)); - }, - fetchUsersByLink: (link: string) => { - dispatch(fetchUsersByLink(link)); + fetchUsersByPage: (link: string, page: number, filter?: string) => { + dispatch(fetchUsersByPage(link, page, filter)); } }; }; diff --git a/scm-ui/src/users/modules/users.js b/scm-ui/src/users/modules/users.js index a93d082f58..c83a4d9b94 100644 --- a/scm-ui/src/users/modules/users.js +++ b/scm-ui/src/users/modules/users.js @@ -43,9 +43,14 @@ export function fetchUsers(link: string) { return fetchUsersByLink(link); } -export function fetchUsersByPage(link: string, page: number) { +export function fetchUsersByPage(link: string, page: number, filter?: string) { // backend start counting by 0 - return fetchUsersByLink(link + "?page=" + (page - 1)); + if (filter) { + return fetchUsersByLink( + `${link}?page=${page - 1}&q=${decodeURIComponent(filter)}` + ); + } + return fetchUsersByLink(`${link}?page=${page - 1}`); } export function fetchUsersByLink(link: string) { @@ -153,9 +158,7 @@ export function createUser(link: string, user: User, callback?: () => void) { callback(); } }) - .catch(error => - dispatch(createUserFailure(error)) - ); + .catch(error => dispatch(createUserFailure(error))); }; } diff --git a/scm-ui/styles/scm.scss b/scm-ui/styles/scm.scss index c2d9183916..d481445db6 100644 --- a/scm-ui/styles/scm.scss +++ b/scm-ui/styles/scm.scss @@ -49,13 +49,22 @@ hr.header-with-actions { display: none; } } -.is-mobile-create-button-spacing { +.is-mobile-action-spacing { @media screen and (max-width: 768px) { - border: 2px solid #e9f7fd; - padding: 1em 1em; - margin-top: 0 !important; - width: 100%; - text-align: center !important; + display: flow-root !important; + + .input-field { + padding: 0; + margin: 0 0 1.25rem 0 !important; + width: 100%; + } + .input-button { + border: 2px solid #e9f7fd; + padding: 1em 1em; + margin-top: 0 !important; + width: 100%; + text-align: center !important; + } } } diff --git a/scm-ui/yarn.lock b/scm-ui/yarn.lock index 3986930a35..f56b234d2c 100644 --- a/scm-ui/yarn.lock +++ b/scm-ui/yarn.lock @@ -698,9 +698,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.27": - version "0.0.27" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.27.tgz#3ed2c7826780b9a1a9ea90464332640cfb5d54b5" +"@scm-manager/ui-bundler@^0.0.28": + version "0.0.28" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.28.tgz#69df46f3bc8fc35ecff0d575d893704b7f731e1e" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" diff --git a/scm-webapp/src/main/doc/enunciate.xml b/scm-webapp/src/main/doc/enunciate.xml index 6e7fc218aa..225d2e0a2c 100644 --- a/scm-webapp/src/main/doc/enunciate.xml +++ b/scm-webapp/src/main/doc/enunciate.xml @@ -52,12 +52,7 @@ ]]> </description> - <api-classes> - <exclude pattern="sonia.scm.debug.DebugResource" /> - <exclude pattern="sonia.scm.api.rest.resources.ConfigurationResource" /> - <exclude pattern="sonia.scm.api.rest.resources.SupportResource" /> - <exclude pattern="sonia.scm.api.rest.resources.RepositoryRootResource" /> - </api-classes> + <api-classes/> <modules> diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java deleted file mode 100644 index dfc0bd2a5d..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/AbstractManagerResource.java +++ /dev/null @@ -1,581 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.net.UrlEscapers; -import org.apache.shiro.authz.AuthorizationException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.LastModifiedAware; -import sonia.scm.Manager; -import sonia.scm.ModelObject; -import sonia.scm.PageResult; -import sonia.scm.api.rest.RestExceptionResult; -import sonia.scm.util.AssertUtil; -import sonia.scm.util.Comparables; -import sonia.scm.util.Util; - -import javax.ws.rs.core.CacheControl; -import javax.ws.rs.core.EntityTag; -import javax.ws.rs.core.GenericEntity; -import javax.ws.rs.core.Request; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.Status; -import javax.ws.rs.core.UriInfo; -import java.net.URI; -import java.util.Collection; -import java.util.Comparator; -import java.util.Date; - -//~--- JDK imports ------------------------------------------------------------ - -public abstract class AbstractManagerResource<T extends ModelObject> { - - /** the logger for AbstractManagerResource */ - private static final Logger logger = - LoggerFactory.getLogger(AbstractManagerResource.class); - - protected final Manager<T> manager; - private final Class<T> type; - - protected int cacheMaxAge = 0; - protected boolean disableCache = false; - - public AbstractManagerResource(Manager<T> manager, Class<T> type) { - this.manager = manager; - this.type = type; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param items - * - * @return - */ - protected abstract GenericEntity<Collection<T>> createGenericEntity( - Collection<T> items); - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param item - * - * @return - */ - protected abstract String getId(T item); - - /** - * Method description - * - * - * @return - */ - protected abstract String getPathPart(); - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * - * @param uriInfo - * @param item - * - * @return - */ - public Response create(UriInfo uriInfo, T item) - { - preCreate(item); - - Response response; - - try - { - manager.create(item); - - String id = getId(item); - response = Response.created(location(uriInfo, id)).build(); - } - catch (AuthorizationException ex) - { - logger.warn("create is not allowd", ex); - response = Response.status(Status.FORBIDDEN).build(); - } - catch (Exception ex) - { - logger.error("error during create", ex); - response = createErrorResponse(ex); - } - - return response; - } - - @VisibleForTesting - URI location(UriInfo uriInfo, String id) { - String escaped = UrlEscapers.urlPathSegmentEscaper().escape(id); - return uriInfo.getAbsolutePath().resolve(getPathPart().concat("/").concat(escaped)); - } - - /** - * Method description - * - * - * @param name - * - * @return - */ - public Response delete(String name) - { - Response response = null; - T item = manager.get(name); - - if (item != null) - { - preDelete(item); - - try - { - manager.delete(item); - response = Response.noContent().build(); - } - catch (AuthorizationException ex) - { - logger.warn("delete not allowd", ex); - response = Response.status(Response.Status.FORBIDDEN).build(); - } - catch (Exception ex) - { - logger.error("error during delete", ex); - response = createErrorResponse(ex); - } - } - - return response; - } - - /** - * Method description - * - * - * - * - * @param name - * @param item - * - * - * @return - */ - public Response update(String name, T item) - { - Response response = null; - - preUpdate(item); - - try - { - manager.modify(item); - response = Response.noContent().build(); - } - catch (AuthorizationException ex) - { - logger.warn("update not allowed", ex); - response = Response.status(Response.Status.FORBIDDEN).build(); - } - catch (Exception ex) - { - logger.error("error during update", ex); - response = createErrorResponse(ex); - } - - return response; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * - * @param request - * @param id - * - * @return - */ - public Response get(Request request, String id) - { - Response response; - T item = manager.get(id); - - if (item != null) - { - prepareForReturn(item); - - if (disableCache) - { - response = Response.ok(item).build(); - } - else - { - response = createCacheResponse(request, item, item); - } - } - else - { - response = Response.status(Response.Status.NOT_FOUND).build(); - } - - return response; - } - - /** - * Method description - * - * - * - * @param request - * @param start - * @param limit - * @param sortby - * @param desc - * @return - */ - public Response getAll(Request request, int start, int limit, String sortby, - boolean desc) - { - Collection<T> items = fetchItems(sortby, desc, start, limit); - - if (Util.isNotEmpty(items)) - { - items = prepareForReturn(items); - } - - Response response = null; - Object entity = createGenericEntity(items); - - if (disableCache) - { - response = Response.ok(entity).build(); - } - else - { - response = createCacheResponse(request, manager, items, entity); - } - - return response; - } - - /** - * Method description - * - * - * @return - */ - public int getCacheMaxAge() - { - return cacheMaxAge; - } - - /** - * Method description - * - * - * @return - */ - public boolean isDisableCache() - { - return disableCache; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param cacheMaxAge - */ - public void setCacheMaxAge(int cacheMaxAge) - { - this.cacheMaxAge = cacheMaxAge; - } - - /** - * Method description - * - * - * @param disableCache - */ - public void setDisableCache(boolean disableCache) - { - this.disableCache = disableCache; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param throwable - * - * @return - */ - protected Response createErrorResponse(Throwable throwable) - { - return createErrorResponse(Status.INTERNAL_SERVER_ERROR, - throwable.getMessage(), throwable); - } - - /** - * Method description - * - * - * @param status - * @param message - * @param throwable - * - * @return - */ - protected Response createErrorResponse(Status status, String message, - Throwable throwable) - { - return Response.status(status).entity(new RestExceptionResult(message, - throwable)).build(); - } - - /** - * Method description - * - * - * @param item - */ - protected void preCreate(T item) {} - - /** - * Method description - * - * - * @param item - */ - protected void preDelete(T item) {} - - /** - * Method description - * - * - * @param item - */ - protected void preUpdate(T item) {} - - /** - * Method description - * - * - * @param item - * - * @return - */ - protected T prepareForReturn(T item) - { - return item; - } - - /** - * Method description - * - * - * @param items - * - * @return - */ - protected Collection<T> prepareForReturn(Collection<T> items) - { - return items; - } - - /** - * Method description - * - * - * @param rb - */ - private void addCacheControl(Response.ResponseBuilder rb) - { - CacheControl cc = new CacheControl(); - - cc.setMaxAge(cacheMaxAge); - rb.cacheControl(cc); - } - - /** - * Method description - * - * - * @param request - * @param timeItem - * @param item - * @param <I> - * - * @return - */ - private <I> Response createCacheResponse(Request request, - LastModifiedAware timeItem, I item) - { - return createCacheResponse(request, timeItem, item, item); - } - - /** - * Method description - * - * - * @param request - * @param timeItem - * @param entityItem - * @param item - * @param <I> - * - * @return - */ - private <I> Response createCacheResponse(Request request, - LastModifiedAware timeItem, Object entityItem, I item) - { - Response.ResponseBuilder builder = null; - Date lastModified = getLastModified(timeItem); - EntityTag e = new EntityTag(Integer.toString(entityItem.hashCode())); - - if (lastModified != null) - { - builder = request.evaluatePreconditions(lastModified, e); - } - else - { - builder = request.evaluatePreconditions(e); - } - - if (builder == null) - { - builder = Response.ok(item).tag(e).lastModified(lastModified); - } - - addCacheControl(builder); - - return builder.build(); - } - - private Comparator<T> createComparator(String sortBy, boolean desc) { - Comparator<T> comparator = Comparables.comparator(type, sortBy); - if (desc) { - comparator = comparator.reversed(); - } - return comparator; - } - - private Collection<T> fetchItems(String sortBy, boolean desc, int start, - int limit) - { - AssertUtil.assertPositive(start); - - Collection<T> items = null; - - if (limit > 0) - { - if (Util.isEmpty(sortBy)) - { - - // replace with something useful - sortBy = "id"; - } - - items = manager.getAll(createComparator(sortBy, desc), start, limit); - } - else if (Util.isNotEmpty(sortBy)) - { - items = manager.getAll(createComparator(sortBy, desc)); - } - else - { - items = manager.getAll(); - } - - return items; - } - - protected PageResult<T> fetchPage(String sortBy, boolean desc, int pageNumber, - int pageSize) { - AssertUtil.assertPositive(pageNumber); - AssertUtil.assertPositive(pageSize); - - if (Util.isEmpty(sortBy)) { - // replace with something useful - sortBy = "id"; - } - - return manager.getPage(createComparator(sortBy, desc), pageNumber, pageSize); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param item - * - * @return - */ - private Date getLastModified(LastModifiedAware item) - { - Date lastModified = null; - Long l = item.getLastModified(); - - if (l != null) - { - lastModified = new Date(l); - } - - return lastModified; - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java deleted file mode 100644 index 79b5dbc2ae..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/BrowserStreamingOutput.java +++ /dev/null @@ -1,37 +0,0 @@ -package sonia.scm.api.rest.resources; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.repository.api.CatCommandBuilder; -import sonia.scm.repository.api.RepositoryService; -import sonia.scm.util.IOUtil; - -import javax.ws.rs.core.StreamingOutput; -import java.io.IOException; -import java.io.OutputStream; - -public class BrowserStreamingOutput implements StreamingOutput { - - private static final Logger logger = - LoggerFactory.getLogger(BrowserStreamingOutput.class); - - private final CatCommandBuilder builder; - private final String path; - private final RepositoryService repositoryService; - - public BrowserStreamingOutput(RepositoryService repositoryService, - CatCommandBuilder builder, String path) { - this.repositoryService = repositoryService; - this.builder = builder; - this.path = path; - } - - @Override - public void write(OutputStream output) throws IOException { - try { - builder.retriveContent(output, path); - } finally { - IOUtil.close(repositoryService); - } - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java deleted file mode 100644 index 828cbf8164..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/ChangePasswordResource.java +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; -import com.webcohesion.enunciate.metadata.rs.TypeHint; -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.authc.credential.PasswordService; -import org.apache.shiro.subject.Subject; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.api.rest.RestActionResult; -import sonia.scm.security.Role; -import sonia.scm.security.ScmSecurityException; -import sonia.scm.user.User; -import sonia.scm.user.UserManager; -import sonia.scm.util.AssertUtil; - -import javax.ws.rs.FormParam; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * Resource to change the password of the authenticated user. - * - * @author Sebastian Sdorra - */ -@Path("action/change-password") -public class ChangePasswordResource -{ - - /** the logger for ChangePasswordResource */ - private static final Logger logger = - LoggerFactory.getLogger(ChangePasswordResource.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param userManager - * @param encryptionHandler - */ - @Inject - public ChangePasswordResource(UserManager userManager, - PasswordService encryptionHandler) - { - this.userManager = userManager; - this.passwordService = encryptionHandler; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Changes the password of the current user. - * - * @param oldPassword old password of the current user - * @param newPassword new password for the current user - */ - @POST - @TypeHint(RestActionResult.class) - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 400, condition = "bad request, the old password is not correct"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public Response changePassword(@FormParam("old-password") String oldPassword, @FormParam("new-password") String newPassword) { - AssertUtil.assertIsNotEmpty(oldPassword); - AssertUtil.assertIsNotEmpty(newPassword); - - int length = newPassword.length(); - - if ((length < 6) || (length > 32)) - { - throw new WebApplicationException(Response.Status.BAD_REQUEST); - } - - Response response = null; - Subject subject = SecurityUtils.getSubject(); - - if (!subject.hasRole(Role.USER)) - { - throw new ScmSecurityException("user is not authenticated"); - } - - User currentUser = subject.getPrincipals().oneByType(User.class); - - if (logger.isInfoEnabled()) - { - logger.info("password change for user {}", currentUser.getName()); - } - - // Only account of the default type can change their password - if (currentUser.getType().equals(userManager.getDefaultType())) - { - User dbUser = userManager.get(currentUser.getName()); - - if (passwordService.passwordsMatch(oldPassword, dbUser.getPassword())) - { - dbUser.setPassword(passwordService.encryptPassword(newPassword)); - userManager.modify(dbUser); - response = Response.ok(new RestActionResult(true)).build(); - } - else - { - response = Response.status(Response.Status.BAD_REQUEST).build(); - } - } - else - { - //J- - logger.error( - "Only account of the default type ({}) can change their password", - userManager.getDefaultType() - ); - //J+ - response = Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); - } - - return response; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final PasswordService passwordService; - - /** Field description */ - private final UserManager userManager; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java deleted file mode 100644 index b7f994b967..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/DiffStreamingOutput.java +++ /dev/null @@ -1,109 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import sonia.scm.repository.api.DiffCommandBuilder; -import sonia.scm.repository.api.RepositoryService; -import sonia.scm.util.IOUtil; - -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.StreamingOutput; -import java.io.IOException; -import java.io.OutputStream; - -//~--- JDK imports ------------------------------------------------------------ - -/** - * - * @author Sebastian Sdorra - */ -public class DiffStreamingOutput implements StreamingOutput -{ - - /** the logger for DiffStreamingOutput */ - private static final Logger logger = - LoggerFactory.getLogger(DiffStreamingOutput.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * - * @param repositoryService - * @param builder - */ - public DiffStreamingOutput(RepositoryService repositoryService, - DiffCommandBuilder builder) - { - this.repositoryService = repositoryService; - this.builder = builder; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param output - * - * @throws IOException - * @throws WebApplicationException - */ - @Override - public void write(OutputStream output) throws IOException { - try - { - builder.retrieveContent(output); - } - finally - { - IOUtil.close(repositoryService); - } - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final DiffCommandBuilder builder; - - /** Field description */ - private final RepositoryService repositoryService; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java deleted file mode 100644 index d5ea6c88de..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java +++ /dev/null @@ -1,213 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.api.rest.resources; - -import com.google.common.base.Function; -import com.google.common.collect.Collections2; -import com.google.common.collect.Maps; -import com.google.common.collect.Ordering; -import com.google.inject.Inject; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryManager; -import sonia.scm.repository.RepositoryTypePredicate; -import sonia.scm.template.Viewable; - -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import java.io.IOException; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.Map; - -/** - * - * @author Sebastian Sdorra - */ -@Path("help/repository-root/{type}.html") -public class RepositoryRootResource -{ - - private static final String TEMPLATE = "/templates/repository-root.mustache"; - - private final RepositoryManager repositoryManager; - - /** - * Constructs ... - * - * @param repositoryManager - */ - @Inject - public RepositoryRootResource(RepositoryManager repositoryManager) - { - this.repositoryManager = repositoryManager; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * - * @param request - * @param type - * - * @return - * - * @throws IOException - */ - @GET - @Produces(MediaType.TEXT_HTML) - public Viewable renderRepositoriesRoot(@Context HttpServletRequest request, @PathParam("type") final String type) - { - //J- - Collection<RepositoryTemplateElement> unsortedRepositories = - Collections2.transform( - Collections2.filter( - repositoryManager.getAll(), new RepositoryTypePredicate(type)) - , new RepositoryTransformFunction() - ); - - List<RepositoryTemplateElement> repositories = Ordering.from( - new RepositoryTemplateElementComparator() - ).sortedCopy(unsortedRepositories); - //J+ - Map<String, Object> environment = Maps.newHashMap(); - - environment.put("repositories", repositories); - - return new Viewable(TEMPLATE, environment); - } - - //~--- inner classes -------------------------------------------------------- - - /** - * Class description - * - * - * @version Enter version here..., 12/05/28 - * @author Enter your name here... - */ - public static class RepositoryTemplateElement - { - - public RepositoryTemplateElement(Repository repository) - { - this.repository = repository; - } - - //~--- get methods -------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getName() - { - return repository.getName(); - } - - /** - * Method description - * - * - * @return - */ - public Repository getRepository() - { - return repository; - } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - private Repository repository; - - } - - - /** - * Class description - * - * - * @version Enter version here..., 12/05/29 - * @author Enter your name here... - */ - private static class RepositoryTemplateElementComparator - implements Comparator<RepositoryTemplateElement> - { - - /** - * Method description - * - * - * @param left - * @param right - * - * @return - */ - @Override - public int compare(RepositoryTemplateElement left, - RepositoryTemplateElement right) - { - return left.getName().compareTo(right.getName()); - } - } - - - /** - * Class description - * - * - * @version Enter version here..., 12/05/28 - * @author Enter your name here... - */ - private static class RepositoryTransformFunction - implements Function<Repository, RepositoryTemplateElement> - { - @Override - public RepositoryTemplateElement apply(Repository repository) - { - return new RepositoryTemplateElement(repository); - } - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SearchResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SearchResource.java deleted file mode 100644 index f22e6cf8c4..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/SearchResource.java +++ /dev/null @@ -1,216 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.api.rest.resources; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.github.legman.Subscribe; - -import com.google.common.base.Function; -import com.google.inject.Inject; -import com.google.inject.Singleton; -import com.webcohesion.enunciate.metadata.rs.ResponseCode; -import com.webcohesion.enunciate.metadata.rs.StatusCodes; - -import sonia.scm.cache.Cache; -import sonia.scm.cache.CacheManager; -import sonia.scm.group.Group; -import sonia.scm.group.GroupEvent; -import sonia.scm.group.GroupManager; -import sonia.scm.search.SearchHandler; -import sonia.scm.search.SearchResult; -import sonia.scm.search.SearchResults; -import sonia.scm.user.User; -import sonia.scm.user.UserEvent; -import sonia.scm.user.UserManager; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; -import javax.ws.rs.core.MediaType; - -/** - * RESTful Web Service Resource to search users and groups. This endpoint can be used to implement typeahead input - * fields for permissions. - * - * @author Sebastian Sdorra - */ -@Singleton -@Path("search") -public class SearchResource -{ - - /** Field description */ - public static final String CACHE_GROUP = "sonia.cache.search.groups"; - - /** Field description */ - public static final String CACHE_USER = "sonia.cache.search.users"; - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param userManager - * @param groupManager - * @param cacheManager - */ - @Inject - public SearchResource(UserManager userManager, GroupManager groupManager, - CacheManager cacheManager) - { - - // create user searchhandler - Cache<String, SearchResults> userCache = cacheManager.getCache(CACHE_USER); - - this.userSearchHandler = new SearchHandler<User>(userCache, userManager); - - // create group searchhandler - Cache<String, SearchResults> groupCache = - cacheManager.getCache(CACHE_GROUP); - - this.groupSearchHandler = new SearchHandler<Group>(groupCache, - groupManager); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param event - */ - @Subscribe - public void onEvent(UserEvent event) - { - if (event.getEventType().isPost()) - { - userSearchHandler.clearCache(); - } - } - - /** - * Method description - * - * - * @param event - */ - @Subscribe - public void onEvent(GroupEvent event) - { - if (event.getEventType().isPost()) - { - groupSearchHandler.clearCache(); - } - } - - /** - * Returns a list of groups found by the given search string. - * - * @param queryString the search string - * - * @return - */ - @GET - @Path("groups") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public SearchResults searchGroups(@QueryParam("query") String queryString) - { - return groupSearchHandler.search(queryString, - new Function<Group, SearchResult>() - { - @Override - public SearchResult apply(Group group) - { - String label = group.getName(); - String description = group.getDescription(); - - if (description != null) - { - label = label.concat(" (").concat(description).concat(")"); - } - - return new SearchResult(group.getName(), label); - } - }); - } - - /** - * Returns a list of users found by the given search string. - * - * @param queryString the search string - * - * @return - */ - @GET - @Path("users") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML }) - public SearchResults searchUsers(@QueryParam("query") String queryString) - { - return userSearchHandler.search(queryString, - new Function<User, SearchResult>() - { - @Override - public SearchResult apply(User user) - { - StringBuilder label = new StringBuilder(user.getName()); - - label.append(" (").append(user.getDisplayName()).append(")"); - - return new SearchResult(user.getName(), label.toString()); - } - }); - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final SearchHandler<Group> groupSearchHandler; - - /** Field description */ - private final SearchHandler<User> userSearchHandler; -} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java index 052bf771b1..1788ca2643 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapter.java @@ -4,13 +4,15 @@ import de.otto.edison.hal.HalRepresentation; import sonia.scm.Manager; import sonia.scm.ModelObject; import sonia.scm.PageResult; -import sonia.scm.api.rest.resources.AbstractManagerResource; +import sonia.scm.util.AssertUtil; +import sonia.scm.util.Comparables; +import sonia.scm.util.Util; -import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Response; import java.net.URI; -import java.util.Collection; +import java.util.Comparator; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import static javax.ws.rs.core.Response.Status.BAD_REQUEST; @@ -27,21 +29,46 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? class CollectionResourceManagerAdapter<MODEL_OBJECT extends ModelObject, - DTO extends HalRepresentation> extends AbstractManagerResource<MODEL_OBJECT> { + DTO extends HalRepresentation>{ + + protected final Manager<MODEL_OBJECT> manager; + protected final Class<MODEL_OBJECT> type; CollectionResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type) { - super(manager, type); + this.manager = manager; + this.type = type; } /** * Reads all model objects in a paged way, maps them using the given function and returns a corresponding http response. * This handles all corner cases, eg. missing privileges. */ - public Response getAll(int page, int pageSize, String sortBy, boolean desc, Function<PageResult<MODEL_OBJECT>, CollectionDto> mapToDto) { - PageResult<MODEL_OBJECT> pageResult = fetchPage(sortBy, desc, page, pageSize); + public Response getAll(int page, int pageSize, Predicate<MODEL_OBJECT> filter, String sortBy, boolean desc, Function<PageResult<MODEL_OBJECT>, CollectionDto> mapToDto) { + PageResult<MODEL_OBJECT> pageResult = fetchPage(filter, sortBy, desc, page, pageSize); return Response.ok(mapToDto.apply(pageResult)).build(); } + private PageResult<MODEL_OBJECT> fetchPage(Predicate<MODEL_OBJECT> filter, String sortBy, boolean desc, int pageNumber, + int pageSize) { + AssertUtil.assertPositive(pageNumber); + AssertUtil.assertPositive(pageSize); + + if (Util.isEmpty(sortBy)) { + // replace with something useful + sortBy = "id"; + } + + return manager.getPage(filter, createComparator(sortBy, desc), pageNumber, pageSize); + } + + private Comparator<MODEL_OBJECT> createComparator(String sortBy, boolean desc) { + Comparator<MODEL_OBJECT> comparator = Comparables.comparator(type, sortBy); + if (desc) { + comparator = comparator.reversed(); + } + return comparator; + } + /** * Creates a model object for the given dto and returns a corresponding http response. * This handles all corner cases, eg. no conflicts or missing privileges. @@ -55,18 +82,7 @@ class CollectionResourceManagerAdapter<MODEL_OBJECT extends ModelObject, return Response.created(URI.create(uriCreator.apply(created))).build(); } - @Override - protected GenericEntity<Collection<MODEL_OBJECT>> createGenericEntity(Collection<MODEL_OBJECT> modelObjects) { - throw new UnsupportedOperationException(); - } - - @Override protected String getId(MODEL_OBJECT item) { return item.getId(); } - - @Override - protected String getPathPart() { - throw new UnsupportedOperationException(); - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java index 6c13dc33a5..b49ad57297 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/GroupCollectionResource.java @@ -7,6 +7,8 @@ import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.group.Group; import sonia.scm.group.GroupManager; +import sonia.scm.search.SearchRequest; +import sonia.scm.search.SearchUtil; import sonia.scm.web.VndMediaType; import javax.inject.Inject; @@ -19,6 +21,9 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import java.util.function.Predicate; + +import static com.google.common.base.Strings.isNullOrEmpty; public class GroupCollectionResource { @@ -63,8 +68,10 @@ public class GroupCollectionResource { @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @QueryParam("sortBy") String sortBy, @DefaultValue("false") - @QueryParam("desc") boolean desc) { - return adapter.getAll(page, pageSize, sortBy, desc, + @QueryParam("desc") boolean desc, + @DefaultValue("") @QueryParam("q") String search + ) { + return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc, pageResult -> groupCollectionToDtoMapper.map(page, pageSize, pageResult)); } @@ -90,4 +97,12 @@ public class GroupCollectionResource { () -> dtoToGroupMapper.map(group), g -> resourceLinks.group().self(g.getName())); } + + private Predicate<Group> createSearchPredicate(String search) { + if (isNullOrEmpty(search)) { + return group -> true; + } + SearchRequest searchRequest = new SearchRequest(search, true); + return group -> SearchUtil.matchesOne(searchRequest, group.getName(), group.getDescription()); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java index 2b2a4cf0ad..79a0e038d9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IdResourceManagerAdapter.java @@ -45,8 +45,8 @@ class IdResourceManagerAdapter<MODEL_OBJECT extends ModelObject, ); } - public Response getAll(int page, int pageSize, String sortBy, boolean desc, Function<PageResult<MODEL_OBJECT>, CollectionDto> mapToDto) { - return collectionAdapter.getAll(page, pageSize, sortBy, desc, mapToDto); + public Response getAll(int page, int pageSize, Predicate<MODEL_OBJECT> filter, String sortBy, boolean desc, Function<PageResult<MODEL_OBJECT>, CollectionDto> mapToDto) { + return collectionAdapter.getAll(page, pageSize, filter, sortBy, desc, mapToDto); } public Response create(DTO dto, Supplier<MODEL_OBJECT> modelObjectSupplier, Function<MODEL_OBJECT, String> uriCreator) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java index e1e1260a4d..b4d148b4bf 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionResource.java @@ -9,6 +9,8 @@ import org.apache.shiro.SecurityUtils; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryPermission; +import sonia.scm.search.SearchRequest; +import sonia.scm.search.SearchUtil; import sonia.scm.user.User; import sonia.scm.web.VndMediaType; @@ -23,6 +25,9 @@ import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import java.util.function.Predicate; + +import static com.google.common.base.Strings.isNullOrEmpty; import static java.util.Collections.singletonList; public class RepositoryCollectionResource { @@ -65,8 +70,10 @@ public class RepositoryCollectionResource { public Response getAll(@DefaultValue("0") @QueryParam("page") int page, @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc) { - return adapter.getAll(page, pageSize, sortBy, desc, + @DefaultValue("false") @QueryParam("desc") boolean desc, + @DefaultValue("") @QueryParam("q") String search + ) { + return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc, pageResult -> repositoryCollectionToDtoMapper.map(page, pageSize, pageResult)); } @@ -106,4 +113,12 @@ public class RepositoryCollectionResource { private String currentUser() { return SecurityUtils.getSubject().getPrincipals().oneByType(User.class).getName(); } + + private Predicate<Repository> createSearchPredicate(String search) { + if (isNullOrEmpty(search)) { + return user -> true; + } + SearchRequest searchRequest = new SearchRequest(search, true); + return repository -> SearchUtil.matchesOne(searchRequest, repository.getName(), repository.getNamespace(), repository.getDescription()); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java index 72507562dd..9c6c0300d6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/SingleResourceManagerAdapter.java @@ -5,11 +5,8 @@ import sonia.scm.ConcurrentModificationException; import sonia.scm.Manager; import sonia.scm.ModelObject; import sonia.scm.NotFoundException; -import sonia.scm.api.rest.resources.AbstractManagerResource; -import javax.ws.rs.core.GenericEntity; import javax.ws.rs.core.Response; -import java.util.Collection; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; @@ -29,10 +26,11 @@ import static javax.ws.rs.core.Response.Status.BAD_REQUEST; */ @SuppressWarnings("squid:S00119") // "MODEL_OBJECT" is much more meaningful than "M", right? class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject, - DTO extends HalRepresentation> extends AbstractManagerResource<MODEL_OBJECT> { + DTO extends HalRepresentation> { private final Function<Throwable, Optional<Response>> errorHandler; - private final Class<MODEL_OBJECT> type; + protected final Manager<MODEL_OBJECT> manager; + protected final Class<MODEL_OBJECT> type; SingleResourceManagerAdapter(Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type) { this(manager, type, e -> Optional.empty()); @@ -42,7 +40,7 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject, Manager<MODEL_OBJECT> manager, Class<MODEL_OBJECT> type, Function<Throwable, Optional<Response>> errorHandler) { - super(manager, type); + this.manager = manager; this.errorHandler = errorHandler; this.type = type; } @@ -72,7 +70,16 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject, else if (modelObjectWasModifiedConcurrently(existingModelObject, changedModelObject)) { throw new ConcurrentModificationException(type, keyExtractor.apply(existingModelObject)); } - return update(getId(existingModelObject), changedModelObject); + return update(changedModelObject); + } + + private Response update(MODEL_OBJECT item) { + try { + manager.modify(item); + return Response.noContent().build(); + } catch (RuntimeException ex) { + return createErrorResponse(ex); + } } private boolean modelObjectWasModifiedConcurrently(MODEL_OBJECT existing, MODEL_OBJECT updated) { @@ -89,23 +96,27 @@ class SingleResourceManagerAdapter<MODEL_OBJECT extends ModelObject, } } - @Override - protected Response createErrorResponse(Throwable throwable) { - return errorHandler.apply(throwable).orElse(super.createErrorResponse(throwable)); + public Response delete(String name) { + MODEL_OBJECT item = manager.get(name); + + if (item != null) { + try { + manager.delete(item); + return Response.noContent().build(); + } catch (RuntimeException ex) { + return createErrorResponse(ex); + } + } else { + return Response.noContent().build(); + } } - @Override - protected GenericEntity<Collection<MODEL_OBJECT>> createGenericEntity(Collection<MODEL_OBJECT> modelObjects) { - throw new UnsupportedOperationException(); + private Response createErrorResponse(RuntimeException throwable) { + return errorHandler.apply(throwable) + .orElseThrow(() -> throwable); } - @Override protected String getId(MODEL_OBJECT item) { return item.getId(); } - - @Override - protected String getPathPart() { - throw new UnsupportedOperationException(); - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index a7442a2262..3e35b8d4a6 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -6,6 +6,8 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.authc.credential.PasswordService; +import sonia.scm.search.SearchRequest; +import sonia.scm.search.SearchUtil; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; @@ -20,6 +22,9 @@ import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; +import java.util.function.Predicate; + +import static com.google.common.base.Strings.isNullOrEmpty; public class UserCollectionResource { @@ -65,8 +70,10 @@ public class UserCollectionResource { public Response getAll(@DefaultValue("0") @QueryParam("page") int page, @DefaultValue("" + DEFAULT_PAGE_SIZE) @QueryParam("pageSize") int pageSize, @QueryParam("sortBy") String sortBy, - @DefaultValue("false") @QueryParam("desc") boolean desc) { - return adapter.getAll(page, pageSize, sortBy, desc, + @DefaultValue("false") @QueryParam("desc") boolean desc, + @DefaultValue("") @QueryParam("q") String search + ) { + return adapter.getAll(page, pageSize, createSearchPredicate(search), sortBy, desc, pageResult -> userCollectionToDtoMapper.map(page, pageSize, pageResult)); } @@ -93,4 +100,12 @@ public class UserCollectionResource { public Response create(@Valid UserDto user) { return adapter.create(user, () -> dtoToUserMapper.map(user, passwordService.encryptPassword(user.getPassword())), u -> resourceLinks.user().self(u.getName())); } + + private Predicate<User> createSearchPredicate(String search) { + if (isNullOrEmpty(search)) { + return user -> true; + } + SearchRequest searchRequest = new SearchRequest(search, true); + return user -> SearchUtil.matchesOne(searchRequest, user.getName(), user.getDisplayName(), user.getMail()); + } } diff --git a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java index 3320773e57..4fd89d657e 100644 --- a/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java +++ b/scm-webapp/src/main/java/sonia/scm/group/DefaultGroupManager.java @@ -59,6 +59,7 @@ import java.util.Collections; import java.util.Comparator; import java.util.LinkedList; import java.util.List; +import java.util.function.Predicate; //~--- JDK imports ------------------------------------------------------------ @@ -250,7 +251,7 @@ public class DefaultGroupManager extends AbstractGroupManager @Override public Collection<Group> getAll() { - return getAll(null); + return getAll(group -> true, null); } /** @@ -262,14 +263,14 @@ public class DefaultGroupManager extends AbstractGroupManager * @return */ @Override - public Collection<Group> getAll(Comparator<Group> comparator) + public Collection<Group> getAll(Predicate<Group> filter, Comparator<Group> comparator) { List<Group> groups = new ArrayList<>(); PermissionActionCheck<Group> check = GroupPermissions.read(); for (Group group : groupDAO.getAll()) { - if (check.isPermitted(group)) { + if (filter.test(group) && check.isPermitted(group)) { groups.add(group.clone()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 12a7f7cc51..1bcd877620 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -64,6 +64,7 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; +import java.util.function.Predicate; import static sonia.scm.AlreadyExistsException.alreadyExists; import static sonia.scm.ContextEntry.ContextBuilder.entity; @@ -253,13 +254,14 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { } @Override - public Collection<Repository> getAll(Comparator<Repository> comparator) { + public Collection<Repository> getAll(Predicate<Repository> filter, Comparator<Repository> comparator) { List<Repository> repositories = Lists.newArrayList(); PermissionActionCheck<Repository> check = RepositoryPermissions.read(); for (Repository repository : repositoryDAO.getAll()) { if (handlerMap.containsKey(repository.getType()) + && filter.test(repository) && check.isPermitted(repository)) { Repository r = repository.clone(); @@ -276,7 +278,7 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { @Override public Collection<Repository> getAll() { - return getAll(null); + return getAll(repository -> true, null); } diff --git a/scm-webapp/src/main/java/sonia/scm/search/SearchHandler.java b/scm-webapp/src/main/java/sonia/scm/search/SearchHandler.java deleted file mode 100644 index 1f00328246..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/search/SearchHandler.java +++ /dev/null @@ -1,221 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.search; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Function; -import com.google.common.collect.Collections2; -import com.google.common.collect.Lists; - -import org.apache.shiro.SecurityUtils; -import org.apache.shiro.subject.Subject; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.cache.Cache; -import sonia.scm.security.ScmSecurityException; -import sonia.scm.util.Util; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collection; - -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Response.Status; -import sonia.scm.security.Role; - -/** - * - * @author Sebastian Sdorra - * - * @param <T> - */ -public class SearchHandler<T> -{ - - /** the logger for SearchHandler */ - private static final Logger logger = - LoggerFactory.getLogger(SearchHandler.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param securityContextProvider - * @param cache - * @param searchable - */ - public SearchHandler(Cache<String, SearchResults> cache, - Searchable<T> searchable) - { - - this.cache = cache; - this.searchable = searchable; - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ - public void clearCache() - { - this.cache.clear(); - } - - /** - * Method description - * - * - * @param queryString - * @param function - * - * @return - */ - public SearchResults search(String queryString, - Function<T, SearchResult> function) - { - Subject subject = SecurityUtils.getSubject(); - - if (!subject.hasRole(Role.USER)) - { - throw new ScmSecurityException("Authentication is required"); - } - - if (Util.isEmpty(queryString)) - { - throw new WebApplicationException(Status.BAD_REQUEST); - } - - SearchResults result = cache.get(queryString); - - if (result == null) - { - SearchRequest request = new SearchRequest(queryString, ignoreCase); - - request.setMaxResults(maxResults); - - Collection<T> users = searchable.search(request); - - result = new SearchResults(); - - if (Util.isNotEmpty(users)) - { - Collection<SearchResult> resultCollection = - Collections2.transform(users, function); - - result.setSuccess(true); - - // create a copy of the result collection to reduce memory - // use ArrayList instead of ImmutableList for copy, - // because the list must be mutable for decorators - result.setResults(Lists.newArrayList(resultCollection)); - cache.put(queryString, result); - } - } - else if (logger.isDebugEnabled()) - { - logger.debug("return searchresults for {} from cache", queryString); - } - - return result; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public int getMaxResults() - { - return maxResults; - } - - /** - * Method description - * - * - * @return - */ - public boolean isIgnoreCase() - { - return ignoreCase; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param ignoreCase - */ - public void setIgnoreCase(boolean ignoreCase) - { - this.ignoreCase = ignoreCase; - } - - /** - * Method description - * - * - * @param maxResults - */ - public void setMaxResults(int maxResults) - { - this.maxResults = maxResults; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - protected Cache<String, SearchResults> cache; - - /** Field description */ - protected Searchable<T> searchable; - - /** Field description */ - private int maxResults = 5; - - /** Field description */ - private boolean ignoreCase = true; -} diff --git a/scm-webapp/src/main/java/sonia/scm/search/SearchResult.java b/scm-webapp/src/main/java/sonia/scm/search/SearchResult.java deleted file mode 100644 index 3862fafe8b..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/search/SearchResult.java +++ /dev/null @@ -1,117 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.search; - -/** - * - * @author Sebastian Sdorra - */ -public class SearchResult -{ - - /** - * Constructs ... - * - */ - public SearchResult() {} - - /** - * Constructs ... - * - * - * @param value - * @param label - */ - public SearchResult(String value, String label) - { - this.value = value; - this.label = label; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public String getLabel() - { - return label; - } - - /** - * Method description - * - * - * @return - */ - public String getValue() - { - return value; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param label - */ - public void setLabel(String label) - { - this.label = label; - } - - /** - * Method description - * - * - * @param value - */ - public void setValue(String value) - { - this.value = value; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private String label; - - /** Field description */ - private String value; -} diff --git a/scm-webapp/src/main/java/sonia/scm/search/SearchResults.java b/scm-webapp/src/main/java/sonia/scm/search/SearchResults.java deleted file mode 100644 index a26982052a..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/search/SearchResults.java +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.search; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Collection; - -import javax.xml.bind.annotation.XmlRootElement; -import sonia.scm.api.rest.RestActionResult; - -/** - * - * @author Sebastian Sdorra - */ -@XmlRootElement(name = "search-results") -public class SearchResults extends RestActionResult -{ - - /** - * Constructs ... - * - */ - public SearchResults() {} - - /** - * Constructs ... - * - * - * @param success - */ - public SearchResults(boolean success) - { - super(success); - } - - /** - * Constructs ... - * - * - * @param results - */ - public SearchResults(Collection<SearchResult> results) - { - super(true); - this.results = results; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - public Collection<SearchResult> getResults() - { - return results; - } - - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param results - */ - public void setResults(Collection<SearchResult> results) - { - this.results = results; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private Collection<SearchResult> results; -} diff --git a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java index a6c40b24f4..b44db8d62a 100644 --- a/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java +++ b/scm-webapp/src/main/java/sonia/scm/user/DefaultUserManager.java @@ -62,6 +62,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.function.Predicate; /** * @@ -280,7 +281,7 @@ public class DefaultUserManager extends AbstractUserManager @Override public Collection<User> getAll() { - return getAll(null); + return getAll(user -> true, null); } /** @@ -292,13 +293,13 @@ public class DefaultUserManager extends AbstractUserManager * @return */ @Override - public Collection<User> getAll(Comparator<User> comparator) + public Collection<User> getAll(Predicate<User> filter, Comparator<User> comparator) { List<User> users = new ArrayList<>(); PermissionActionCheck<User> check = UserPermissions.read(); for (User user : userDAO.getAll()) { - if (check.isPermitted(user)) { + if (filter.test(user) && check.isPermitted(user)) { users.add(user.clone()); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java deleted file mode 100644 index fd9745be83..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/AbstractManagerResourceTest.java +++ /dev/null @@ -1,164 +0,0 @@ -package sonia.scm.api.rest.resources; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import sonia.scm.Manager; -import sonia.scm.ModelObject; - -import javax.ws.rs.core.GenericEntity; -import javax.ws.rs.core.Request; -import javax.ws.rs.core.UriInfo; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Collection; -import java.util.Comparator; - -import static java.util.Collections.emptyList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class AbstractManagerResourceTest { - - @Mock - private Manager<Simple> manager; - - @Mock - private Request request; - - @Mock - private UriInfo uriInfo; - - @Captor - private ArgumentCaptor<Comparator<Simple>> comparatorCaptor; - - private AbstractManagerResource<Simple> abstractManagerResource; - - @Before - public void captureComparator() { - when(manager.getAll(comparatorCaptor.capture(), eq(0), eq(1))).thenReturn(emptyList()); - abstractManagerResource = new SimpleManagerResource(); - } - - @Test - public void shouldAcceptDefaultSortByParameter() { - abstractManagerResource.getAll(request, 0, 1, null, true); - - Comparator<Simple> comparator = comparatorCaptor.getValue(); - assertTrue(comparator.compare(new Simple("1", null), new Simple("2", null)) > 0); - } - - @Test - public void shouldAcceptValidSortByParameter() { - abstractManagerResource.getAll(request, 0, 1, "data", true); - - Comparator<Simple> comparator = comparatorCaptor.getValue(); - assertTrue(comparator.compare(new Simple("", "1"), new Simple("", "2")) > 0); - } - - @Test(expected = IllegalArgumentException.class) - public void shouldFailForIllegalSortByParameter() { - abstractManagerResource.getAll(request, 0, 1, "x", true); - } - - @Test - public void testLocation() throws URISyntaxException { - URI uri = location("special-item"); - assertEquals(new URI("https://scm.scm-manager.org/simple/special-item"), uri); - } - - @Test - public void testLocationWithSpaces() throws URISyntaxException { - URI uri = location("Scm Special Group"); - assertEquals(new URI("https://scm.scm-manager.org/simple/Scm%20Special%20Group"), uri); - } - - private URI location(String id) throws URISyntaxException { - URI base = new URI("https://scm.scm-manager.org/"); - when(uriInfo.getAbsolutePath()).thenReturn(base); - - return abstractManagerResource.location(uriInfo, id); - } - - private class SimpleManagerResource extends AbstractManagerResource<Simple> { - - { - disableCache = true; - } - - private SimpleManagerResource() { - super(AbstractManagerResourceTest.this.manager, Simple.class); - } - - @Override - protected GenericEntity<Collection<Simple>> createGenericEntity(Collection<Simple> items) { - return null; - } - - @Override - protected String getId(Simple item) { - return null; - } - - @Override - protected String getPathPart() { - return "simple"; - } - } - - public static class Simple implements ModelObject { - - private String id; - private String data; - - Simple(String id, String data) { - this.id = id; - this.data = data; - } - - public String getData() { - return data; - } - - @Override - public String getId() { - return id; - } - - @Override - public void setLastModified(Long timestamp) { - - } - - @Override - public Long getCreationDate() { - return null; - } - - @Override - public void setCreationDate(Long timestamp) { - - } - - @Override - public Long getLastModified() { - return null; - } - - @Override - public String getType() { - return null; - } - @Override - public boolean isValid() { - return false; - } - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/rest/resources/Simple.java b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/Simple.java new file mode 100644 index 0000000000..3887bc4a9c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/rest/resources/Simple.java @@ -0,0 +1,52 @@ +package sonia.scm.api.rest.resources; + +import sonia.scm.ModelObject; + +public class Simple implements ModelObject { + + private String id; + private String data; + + public Simple(String id, String data) { + this.id = id; + this.data = data; + } + + public String getData() { + return data; + } + + @Override + public String getId() { + return id; + } + + @Override + public void setLastModified(Long timestamp) { + + } + + @Override + public Long getCreationDate() { + return null; + } + + @Override + public void setCreationDate(Long timestamp) { + + } + + @Override + public Long getLastModified() { + return null; + } + + @Override + public String getType() { + return null; + } + @Override + public boolean isValid() { + return false; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapterTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapterTest.java new file mode 100644 index 0000000000..1fb0dd76df --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/CollectionResourceManagerAdapterTest.java @@ -0,0 +1,65 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.Manager; +import sonia.scm.api.rest.resources.Simple; + +import java.util.Comparator; +import java.util.function.Predicate; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class CollectionResourceManagerAdapterTest { + + @Mock + private Manager<Simple> manager; + @Captor + private ArgumentCaptor<Comparator<Simple>> comparatorCaptor; + @Captor + private ArgumentCaptor<Predicate<Simple>> filterCaptor; + + private CollectionResourceManagerAdapter<Simple, HalRepresentation> abstractManagerResource; + + @Before + public void captureComparator() { + when(manager.getPage(filterCaptor.capture(), comparatorCaptor.capture(), eq(0), eq(1))).thenReturn(null); + abstractManagerResource = new SimpleManagerResource(); + } + + @Test + public void shouldAcceptDefaultSortByParameter() { + abstractManagerResource.getAll(0, 1, x -> true, null, true, r -> null); + + Comparator<Simple> comparator = comparatorCaptor.getValue(); + assertTrue(comparator.compare(new Simple("1", null), new Simple("2", null)) > 0); + } + + @Test + public void shouldAcceptValidSortByParameter() { + abstractManagerResource.getAll(0, 1, x -> true, "data", true, r -> null); + + Comparator<Simple> comparator = comparatorCaptor.getValue(); + assertTrue(comparator.compare(new Simple("", "1"), new Simple("", "2")) > 0); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldFailForIllegalSortByParameter() { + abstractManagerResource.getAll(0, 1, x -> true, "x", true, r -> null); + } + + private class SimpleManagerResource extends CollectionResourceManagerAdapter<Simple, HalRepresentation> { + private SimpleManagerResource() { + super(CollectionResourceManagerAdapterTest.this.manager, Simple.class); + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index 3e2d0f9663..09236c0e19 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -11,6 +11,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; @@ -30,9 +31,11 @@ import java.net.URISyntaxException; import java.net.URL; import java.util.Collection; import java.util.Collections; +import java.util.function.Predicate; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -67,8 +70,10 @@ public class GroupRootResourceTest { @InjectMocks private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; - - private ArgumentCaptor<Group> groupCaptor = ArgumentCaptor.forClass(Group.class); + @Captor + private ArgumentCaptor<Group> groupCaptor; + @Captor + private ArgumentCaptor<Predicate<Group>> filterCaptor; @Before public void prepareEnvironment() { @@ -77,7 +82,7 @@ public class GroupRootResourceTest { doNothing().when(groupManager).modify(groupCaptor.capture()); Group group = createDummyGroup(); - when(groupManager.getPage(any(), eq(0), eq(10))).thenReturn(new PageResult<>(singletonList(group), 1)); + when(groupManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(new PageResult<>(singletonList(group), 1)); when(groupManager.get("admin")).thenReturn(group); GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks); @@ -317,6 +322,23 @@ public class GroupRootResourceTest { assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/groups/admin\"}")); } + @Test + public void shouldCreateFilterForSearch() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "?q=One"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + Group group = new Group("xml", "someone"); + assertTrue(filterCaptor.getValue().test(group)); + group.setName("nothing"); + group.setDescription("Someone"); + assertTrue(filterCaptor.getValue().test(group)); + group.setDescription("Nobody"); + assertFalse(filterCaptor.getValue().test(group)); + } + @Test public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + GroupRootResource.GROUPS_PATH_V2 + "admin"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index d00d45bb24..55754469ec 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -13,6 +13,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.PageResult; @@ -31,6 +32,7 @@ import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.util.function.Predicate; import static java.util.Collections.singletonList; import static java.util.stream.Stream.of; @@ -42,6 +44,7 @@ import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyObject; @@ -78,6 +81,8 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { @Mock private ScmPathInfo uriInfo; + @Captor + private ArgumentCaptor<Predicate<Repository>> filterCaptor; private final URI baseUri = URI.create("/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @@ -150,7 +155,7 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { @Test public void shouldGetAll() throws URISyntaxException, UnsupportedEncodingException { PageResult<Repository> singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); - when(repositoryManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult); + when(repositoryManager.getPage(any(), any(), eq(0), eq(10))).thenReturn(singletonPageResult); MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2); MockHttpResponse response = new MockHttpResponse(); @@ -161,6 +166,22 @@ public class RepositoryRootResourceTest extends RepositoryTestBase { assertTrue(response.getContentAsString().contains("\"name\":\"repo\"")); } + @Test + public void shouldCreateFilterForSearch() throws URISyntaxException { + PageResult<Repository> singletonPageResult = createSingletonPageResult(mockRepository("space", "repo")); + when(repositoryManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(singletonPageResult); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "?q=Rep"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_OK, response.getStatus()); + assertTrue(filterCaptor.getValue().test(new Repository("x", "git", "all_repos", "x"))); + assertTrue(filterCaptor.getValue().test(new Repository("x", "git", "x", "repository"))); + assertFalse(filterCaptor.getValue().test(new Repository("rep", "rep", "x", "x"))); + } + @Test public void shouldHandleUpdateForNotExistingRepository() throws URISyntaxException, IOException { URL url = Resources.getResource("sonia/scm/api/v2/repository-test-update.json"); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 4047dfadd2..06d9b89120 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -12,6 +12,7 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.ContextEntry; @@ -30,6 +31,7 @@ import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.util.Collection; +import java.util.function.Predicate; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; @@ -72,7 +74,11 @@ public class UserRootResourceTest { @InjectMocks private PermissionCollectionToDtoMapper permissionCollectionToDtoMapper; - private ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class); + @Captor + private ArgumentCaptor<User> userCaptor; + @Captor + private ArgumentCaptor<Predicate<User>> filterCaptor; + private User originalUser; @Before @@ -333,7 +339,7 @@ public class UserRootResourceTest { @Test public void shouldCreatePageForOnePageOnly() throws URISyntaxException, UnsupportedEncodingException { PageResult<User> singletonPageResult = createSingletonPageResult(1); - when(userManager.getPage(any(), eq(0), eq(10))).thenReturn(singletonPageResult); + when(userManager.getPage(any(), any(), eq(0), eq(10))).thenReturn(singletonPageResult); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2); MockHttpResponse response = new MockHttpResponse(); @@ -349,7 +355,7 @@ public class UserRootResourceTest { @Test public void shouldCreatePageForMultiplePages() throws URISyntaxException, UnsupportedEncodingException { PageResult<User> singletonPageResult = createSingletonPageResult(3); - when(userManager.getPage(any(), eq(1), eq(1))).thenReturn(singletonPageResult); + when(userManager.getPage(any(), any(), eq(1), eq(1))).thenReturn(singletonPageResult); MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?page=1&pageSize=1"); MockHttpResponse response = new MockHttpResponse(); @@ -364,6 +370,28 @@ public class UserRootResourceTest { assertTrue(response.getContentAsString().contains("\"last\":{\"href\":\"/v2/users/?page=2")); } + @Test + public void shouldCreateFilterForSearch() throws URISyntaxException { + PageResult<User> singletonPageResult = createSingletonPageResult(1); + when(userManager.getPage(filterCaptor.capture(), any(), eq(0), eq(10))).thenReturn(singletonPageResult); + MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "?q=One"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_OK, response.getStatus()); + User user = new User("Someone I know"); + assertTrue(filterCaptor.getValue().test(user)); + user.setName("nobody"); + user.setDisplayName("Someone I know"); + assertTrue(filterCaptor.getValue().test(user)); + user.setDisplayName("nobody"); + user.setMail("me@someone.com"); + assertTrue(filterCaptor.getValue().test(user)); + user.setMail("me@nowhere.com"); + assertFalse(filterCaptor.getValue().test(user)); + } + @Test public void shouldGetPermissionLink() throws URISyntaxException, UnsupportedEncodingException { MockHttpRequest request = MockHttpRequest.get("/" + UserRootResource.USERS_PATH_V2 + "Neo");