From 9c7c2c9d9a106ed2d3e45047ace4d9b3e8ed8d4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maren=20S=C3=BCwer?= Date: Tue, 31 Jul 2018 13:04:09 +0200 Subject: [PATCH] show group table in ui --- scm-ui/public/locales/en/groups.json | 10 ++ scm-ui/src/createReduxStore.js | 2 + scm-ui/src/groups/components/table/Details.js | 32 +++++ .../src/groups/components/table/GroupRow.js | 25 ++++ .../src/groups/components/table/GroupTable.js | 33 +++++ scm-ui/src/groups/components/table/index.js | 3 + scm-ui/src/groups/containers/Groups.js | 127 +++++++++++++++++- scm-ui/src/groups/modules/groups.js | 67 +++++++++ scm-ui/src/groups/modules/groups.test.js | 104 +++++++++++++- scm-ui/src/groups/types/Group.js | 17 +++ 10 files changed, 413 insertions(+), 7 deletions(-) create mode 100644 scm-ui/public/locales/en/groups.json create mode 100644 scm-ui/src/groups/components/table/Details.js create mode 100644 scm-ui/src/groups/components/table/GroupRow.js create mode 100644 scm-ui/src/groups/components/table/GroupTable.js create mode 100644 scm-ui/src/groups/components/table/index.js create mode 100644 scm-ui/src/groups/types/Group.js diff --git a/scm-ui/public/locales/en/groups.json b/scm-ui/public/locales/en/groups.json new file mode 100644 index 0000000000..8fe3d7e50b --- /dev/null +++ b/scm-ui/public/locales/en/groups.json @@ -0,0 +1,10 @@ +{ + "group": { + "name": "Name", + "description": "Description" + }, + "groups": { + "title": "Groups", + "subtitle": "Create, read, update and delete groups" + } +} diff --git a/scm-ui/src/createReduxStore.js b/scm-ui/src/createReduxStore.js index dd16f9cbfc..0c6e6261f7 100644 --- a/scm-ui/src/createReduxStore.js +++ b/scm-ui/src/createReduxStore.js @@ -5,6 +5,7 @@ import { createStore, compose, applyMiddleware, combineReducers } from "redux"; import { routerReducer, routerMiddleware } from "react-router-redux"; import users from "./users/modules/users"; +import groups from "./groups/modules/groups"; import auth from "./modules/auth"; import pending from "./modules/pending"; import failure from "./modules/failure"; @@ -20,6 +21,7 @@ function createReduxStore(history: BrowserHistory) { pending, failure, users, + groups, auth }); diff --git a/scm-ui/src/groups/components/table/Details.js b/scm-ui/src/groups/components/table/Details.js new file mode 100644 index 0000000000..30c46b78c1 --- /dev/null +++ b/scm-ui/src/groups/components/table/Details.js @@ -0,0 +1,32 @@ +//@flow +import React from "react"; +import type { Group } from "../../types/Group"; +import { translate } from "react-i18next"; +import { Checkbox } from "../../../components/forms"; + +type Props = { + group: Group, + t: string => string +}; + +class Details extends React.Component { + render() { + const { group, t } = this.props; + return ( + + + + + + + + + + + +
{t("group.name")}{group.name}
{t("group.description")}{group.description}
+ ); + } +} + +export default translate("groups")(Details); diff --git a/scm-ui/src/groups/components/table/GroupRow.js b/scm-ui/src/groups/components/table/GroupRow.js new file mode 100644 index 0000000000..9bbca6bb41 --- /dev/null +++ b/scm-ui/src/groups/components/table/GroupRow.js @@ -0,0 +1,25 @@ +// @flow +import React from "react"; +import { Link } from "react-router-dom"; +import type { Group } from "../../types/Group"; + +type Props = { + group: Group +}; + +export default class GroupRow extends React.Component { + renderLink(to: string, label: string) { + return {label}; + } + + render() { + const { group } = this.props; + const to = `/group/${group.name}`; + return ( + + {this.renderLink(to, group.name)} + {this.renderLink(to, group.description)} + + ); + } +} diff --git a/scm-ui/src/groups/components/table/GroupTable.js b/scm-ui/src/groups/components/table/GroupTable.js new file mode 100644 index 0000000000..5078750b8e --- /dev/null +++ b/scm-ui/src/groups/components/table/GroupTable.js @@ -0,0 +1,33 @@ +// @flow +import React from "react"; +import { translate } from "react-i18next"; +import GroupRow from "./GroupRow"; +import type { Group } from "../../types/Group"; + +type Props = { + t: string => string, + groups: Group[] +}; + +class GroupTable extends React.Component { + render() { + const { groups, t } = this.props; + return ( + + + + + + + + + {groups.map((group, index) => { + return ; + })} + +
{t("group.name")}{t("group.description")}
+ ); + } +} + +export default translate("groups")(GroupTable); diff --git a/scm-ui/src/groups/components/table/index.js b/scm-ui/src/groups/components/table/index.js new file mode 100644 index 0000000000..e82be3f5ee --- /dev/null +++ b/scm-ui/src/groups/components/table/index.js @@ -0,0 +1,3 @@ +export { default as Details } from "./Details"; +export { default as GroupRow } from "./GroupRow"; +export { default as GroupTable } from "./GroupTable"; diff --git a/scm-ui/src/groups/containers/Groups.js b/scm-ui/src/groups/containers/Groups.js index 2405cffc34..bf6c89efe2 100644 --- a/scm-ui/src/groups/containers/Groups.js +++ b/scm-ui/src/groups/containers/Groups.js @@ -1,24 +1,139 @@ //@flow import React from "react"; import { connect } from "react-redux"; +import { translate } from "react-i18next"; +import type { Group } from "../types/Group.js"; +import type { PagedCollection } from "../../types/Collection"; +import type { History } from "history"; +import { Page } from "../../components/layout"; +import { GroupTable } from "./../components/table"; +import Paginator from "../../components/Paginator"; -type Props = {}; +import { + fetchGroupsByPage, + fetchGroupsByLink, + getGroupsFromState, + isFetchGroupsPending, + getFetchGroupsFailure, + isPermittedToCreateGroups, + selectListAsCollection +} from "../modules/groups"; + +type Props = { + groups: Group[], + loading: boolean, + error: Error, + canAddGroups: boolean, + list: PagedCollection, + page: number, + + // context objects + t: string => string, + history: History, + + // dispatch functions + fetchGroupsByPage: (page: number) => void, + fetchGroupsByLink: (link: string) => void +}; class Groups extends React.Component { + + componentDidMount() { + this.props.fetchGroupsByPage(this.props.page); + } + + onPageChange = (link: string) => { + this.props.fetchGroupsByLink(link); + }; + + /** + * reflect page transitions in the uri + */ + componentDidUpdate = (prevProps: Props) => { + const { page, list } = this.props; + if (list.page) { + // backend starts paging by 0 + const statePage: number = list.page + 1; + if (page !== statePage) { + this.props.history.push(`/groups/${statePage}`); + } + } + }; + render() { - return "groups will be displayed here"; + const { groups, loading, error, t } = this.props; + return ( + + + {this.renderPaginator()} + {this.renderCreateButton()} + + ); + } + + renderPaginator() { + const { list } = this.props; + if (list) { + return ; + } + return null; + } + + renderCreateButton() { + /* if (this.props.canAddGroups) { + return ; + } else { + return; + }*/ } } -const mapStateToProps = state => { - 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 groups = getGroupsFromState(state); + const loading = isFetchGroupsPending(state); + const error = getFetchGroupsFailure(state); + + const page = getPageFromProps(ownProps); + const canAddGroups = isPermittedToCreateGroups(state); + const list = selectListAsCollection(state); + + return { + groups, + loading, + error, + canAddGroups, + list, + page + }; }; const mapDispatchToProps = (dispatch) => { - return {}; + return { + fetchGroupsByPage: (page: number) => { + dispatch(fetchGroupsByPage(page)); + }, + fetchGroupsByLink: (link: string) => { + dispatch(fetchGroupsByLink(link)); + } + }; }; export default connect( mapStateToProps, mapDispatchToProps -)(Groups); +)(translate("groups")(Groups)); diff --git a/scm-ui/src/groups/modules/groups.js b/scm-ui/src/groups/modules/groups.js index be0fc97615..5b97fae3a2 100644 --- a/scm-ui/src/groups/modules/groups.js +++ b/scm-ui/src/groups/modules/groups.js @@ -5,6 +5,7 @@ import * as types from "../../modules/types"; import { combineReducers, Dispatch } from "redux"; import type { Action } from "../../types/Action"; import type { PagedCollection } from "../../types/Collection"; +import type {Groups} from "../types/Groups"; export const FETCH_GROUPS = "scm/groups/FETCH_GROUPS"; export const FETCH_GROUPS_PENDING = `${FETCH_GROUPS}_${types.PENDING_SUFFIX}`; @@ -154,3 +155,69 @@ export default combineReducers({ list: listReducer, byNames: byNamesReducer }); + + +// selectors + +const selectList = (state: Object) => { + if (state.groups && state.groups.list) { + return state.groups.list; + } + return {}; +}; + +const selectListEntry = (state: Object): Object => { + const list = selectList(state); + if (list.entry) { + return list.entry; + } + return {}; +}; + +export const selectListAsCollection = (state: Object): PagedCollection => { + return selectListEntry(state); +}; + +export const isPermittedToCreateGroups = (state: Object): boolean => { + const permission = selectListEntry(state).groupCreatePermission; + if (permission) { + return true; + } + return false; +}; + +export function getGroupsFromState(state: Object) { + const groupNames = selectList(state).entries; + if (!groupNames) { + return null; + } + const groupEntries: Group[] = []; + + for (let groupName of groupNames) { + groupEntries.push(state.groups.byNames[groupName]); + } + + return groupEntries; +} + +export function isFetchGroupsPending(state: Object) { + return isPending(state, FETCH_GROUPS); +} + +export function getFetchGroupsFailure(state: Object) { + return getFailure(state, FETCH_GROUPS); +} + +export function isCreateGroupPending(state: Object) { + return isPending(state, CREATE_GROUP); +} + +export function getCreateGroupFailure(state: Object) { + return getFailure(state, CREATE_GROUP); +} + +export function getGroupByName(state: Object, name: string) { + if (state.groups && state.groups.byNames) { + return state.groups.byNames[name]; + } +} diff --git a/scm-ui/src/groups/modules/groups.test.js b/scm-ui/src/groups/modules/groups.test.js index da0d3e97de..326452e029 100644 --- a/scm-ui/src/groups/modules/groups.test.js +++ b/scm-ui/src/groups/modules/groups.test.js @@ -5,13 +5,23 @@ import fetchMock from "fetch-mock"; import reducer, { fetchGroups, + FETCH_GROUPS, FETCH_GROUPS_PENDING, FETCH_GROUPS_SUCCESS, FETCH_GROUPS_FAILURE, - fetchGroupsSuccess + isFetchUsersPending, + fetchGroupsSuccess, + isPermittedToCreateGroups, + getGroupsFromState, + getFetchGroupsFailure, + isFetchGroupsPending, + selectListAsCollection } from "./groups" const GROUPS_URL = "/scm/api/rest/v2/groups"; +const error = new Error("You have an error!"); + + const groupZaphod = { creationDate: "2018-07-31T08:39:07.860Z", description: "This is a group", @@ -182,3 +192,95 @@ describe("groups reducer", () => { expect(newState.byNames["zaphodGroup"]).toBeTruthy(); }); }); + + +describe("selector tests", () => { + + it("should return an empty object", () => { + expect(selectListAsCollection({})).toEqual({}); + expect(selectListAsCollection({ groups: { a: "a" } })).toEqual({}); + }); + + it("should return a state slice collection", () => { + const collection = { + page: 3, + totalPages: 42 + }; + + const state = { + groups: { + list: { + entry: collection + } + } + }; + expect(selectListAsCollection(state)).toBe(collection); + }); + + it("should return false", () => { + expect(isPermittedToCreateGroups({})).toBe(false); + expect(isPermittedToCreateGroups({ groups: { list: { entry: {} } } })).toBe( + false + ); + expect( + isPermittedToCreateGroups({ + groups: { list: { entry: { groupCreatePermission: false } } } + }) + ).toBe(false); + }); + + it("should return true", () => { + const state = { + groups: { + list: { + entry: { + groupCreatePermission: true + } + } + } + }; + expect(isPermittedToCreateGroups(state)).toBe(true); + }); + + it("should get groups from state", () => { + const state = { + groups: { + list: { + entries: ["a", "b"] + }, + byNames: { + a: { name: "a" }, + b: { name: "b" } + } + } + }; + expect(getGroupsFromState(state)).toEqual([{ name: "a" }, { name: "b" }]); + }); + + it("should return true, when fetch groups is pending", () => { + const state = { + pending: { + [FETCH_GROUPS]: true + } + }; + expect(isFetchGroupsPending(state)).toEqual(true); + }); + + it("should return false, when fetch groups is not pending", () => { + expect(isFetchGroupsPending({})).toEqual(false); + }); + + it("should return error when fetch groups did fail", () => { + const state = { + failure: { + [FETCH_GROUPS]: error + } + }; + expect(getFetchGroupsFailure(state)).toEqual(error); + }); + + it("should return undefined when fetch users did not fail", () => { + expect(getFetchGroupsFailure({})).toBe(undefined); + }); + +}); diff --git a/scm-ui/src/groups/types/Group.js b/scm-ui/src/groups/types/Group.js new file mode 100644 index 0000000000..f11a524dd8 --- /dev/null +++ b/scm-ui/src/groups/types/Group.js @@ -0,0 +1,17 @@ +//@flow +import type { Links } from "../../types/hal"; +import type { User } from "../../users/types/User"; + +export type Group = { + name: string, + creationDate: string, + description: string, + lastModified: string, + type: string, + properties: [], + members: string[], + _links: Links, + _embedded: { + members: User[] + } +};