From 32f5aa0e6f13e37a58bdc521a81dbeff7ff63343 Mon Sep 17 00:00:00 2001 From: Christie Molloy Date: Mon, 28 Sep 2020 11:58:03 -0400 Subject: [PATCH] First page within Group section (#108) * add code to group section * updates to group section * more updates to groupsection * add Eriks changes * groups broken * more group work * lots of updates * fix bugs * working on typescript and clearing out dead code * fix TS errors and PR feedback * more TS fixes * fixed the TS errors * run prettier * fix errors from yarn checktypes command * fix prettier * fix lint and checktypes * remove isInline * revert prettier * revert prettier commits * update i18 * fix prettier * feedback * fix error --- .eslintrc.json | 72 +- security-admin-console-v2.json | 90 +- src/clients/ClientsSection.tsx | 2 + .../add/__tests__/mock-serverinfo.json | 14538 ++++++++-------- src/clients/messages.json | 3 +- src/components/table-toolbar/TableToolbar.tsx | 44 +- src/groups/GroupsList.tsx | 107 +- src/groups/GroupsSection.css | 10 + src/groups/GroupsSection.tsx | 161 +- src/groups/__tests__/mock-groups.json | 26 + src/groups/messages.json | 14 + src/groups/models/{groups.tsx => groups.ts} | 3 + src/groups/models/server-info.ts | 15 + src/i18n.ts | 11 +- src/realm-roles/__tests__/mock-roles.json | 152 +- src/realm-roles/messages.json | 39 +- src/realm/messages.json | 2 +- yarn.lock | 7 +- 18 files changed, 7783 insertions(+), 7513 deletions(-) create mode 100644 src/groups/GroupsSection.css create mode 100644 src/groups/__tests__/mock-groups.json create mode 100644 src/groups/messages.json rename src/groups/models/{groups.tsx => groups.ts} (81%) create mode 100644 src/groups/models/server-info.ts diff --git a/.eslintrc.json b/.eslintrc.json index c206d8953f..20dbe4ab13 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,38 +1,38 @@ { - "env": { - "browser": true, - "es6": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:@typescript-eslint/eslint-recommended" - ], - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 2018, - "sourceType": "module" - }, - "plugins": [ - "react", - "@typescript-eslint" - ], - "rules": { - "no-unused-vars": "off", - "@typescript-eslint/no-unused-vars": [ - "error" - ] - }, - "settings": { - "react": { - "version": "detect" - } - } + "env": { + "browser": true, + "es6": true + }, + "extends": [ + "eslint:recommended", + "plugin:react/recommended", + "plugin:@typescript-eslint/eslint-recommended" + ], + "globals": { + "Atomics": "readonly", + "SharedArrayBuffer": "readonly" + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + }, + "ecmaVersion": 2018, + "sourceType": "module" + }, + "plugins": [ + "react", + "@typescript-eslint" + ], + "rules": { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": [ + "error" + ] + }, + "settings": { + "react": { + "version": "detect" + } + } } \ No newline at end of file diff --git a/security-admin-console-v2.json b/security-admin-console-v2.json index 2d93472cbf..336558984c 100644 --- a/security-admin-console-v2.json +++ b/security-admin-console-v2.json @@ -1,47 +1,47 @@ { - "clientId": "security-admin-console-v2", - "rootUrl": "http://localhost:8080/", - "adminUrl": "http://localhost:8080/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "redirectUris": [ - "http://localhost:8080/*" - ], - "webOrigins": [ - "http://localhost:8080" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": [ - "web-origins", - "role_list", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ], - "access": { - "view": true, - "configure": true, - "manage": true - } + "clientId": "security-admin-console-v2", + "rootUrl": "http://localhost:8080/", + "adminUrl": "http://localhost:8080/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "http://localhost:8080/*" + ], + "webOrigins": [ + "http://localhost:8080" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ], + "access": { + "view": true, + "configure": true, + "manage": true + } } \ No newline at end of file diff --git a/src/clients/ClientsSection.tsx b/src/clients/ClientsSection.tsx index 1cb07f7e6f..dc23f7b128 100644 --- a/src/clients/ClientsSection.tsx +++ b/src/clients/ClientsSection.tsx @@ -47,6 +47,8 @@ export const ClientsSection = () => { setFirst(first); setMax(max); }} + inputGroupName="clientsToolbarTextInput" + inputGroupPlaceholder={t("Search for client")} toolbarItem={ <> - - - {toolbarItem && {toolbarItem}} + + {inputGroupName && ( + + + + + + + )} + + {toolbarItem} {pagination()} diff --git a/src/groups/GroupsList.tsx b/src/groups/GroupsList.tsx index 352a0ccbfd..3b3b28464d 100644 --- a/src/groups/GroupsList.tsx +++ b/src/groups/GroupsList.tsx @@ -1,35 +1,106 @@ -import React from "react"; +import React, { useState, useEffect, useContext } from "react"; import { Table, TableHeader, TableBody, TableVariant, } from "@patternfly/react-table"; -import { GroupRepresentation } from "./models/groups"; +import { Button } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; +import { GroupRepresentation } from "./models/groups"; +import { UsersIcon } from "@patternfly/react-icons"; +import { HttpClientContext } from "../http-service/HttpClientContext"; type GroupsListProps = { - list: GroupRepresentation[]; + list?: GroupRepresentation[]; }; export const GroupsList = ({ list }: GroupsListProps) => { - const { t } = useTranslation("group"); - const columns: (keyof GroupRepresentation)[] = ["name"]; + const { t } = useTranslation("groups"); + const httpClient = useContext(HttpClientContext)!; + const columnGroupName: keyof GroupRepresentation = "name"; + const columnGroupNumber: keyof GroupRepresentation = "membersLength"; + const [formattedData, setFormattedData] = useState([ + { cells: [], selected: false }, + ]); - const data = list.map((c) => { - return { cells: columns.map((col) => c[col]) }; - }); + const formatData = (data: GroupRepresentation[]) => + data.map((group: { [key: string]: any }, index) => { + const groupName = group[columnGroupName]; + const groupNumber = group[columnGroupNumber]; + return { + cells: [ + , +
+ + {groupNumber} +
, + ], + selected: false, + }; + }); + + useEffect(() => { + setFormattedData(formatData(list!)); + }, [list]); + + function onSelect( + event: React.FormEvent, + isSelected: boolean, + rowId: number + ) { + let localRow; + if (rowId === undefined) { + localRow = formattedData.map((row: { [key: string]: any }) => { + row.selected = isSelected; + return row; + }); + } else { + localRow = [...formattedData]; + localRow[rowId].selected = isSelected; + setFormattedData(localRow); + } + } + + // Delete individual rows using the action in the table + function onDelete(rowIndex: number) { + const localFilteredData = [...list!]; + httpClient.doDelete( + `/admin/realms/master/groups/${localFilteredData[rowIndex].id}` + ); + // TO DO update the state + } + + const tableHeader = [{ title: t("groupName") }, { title: t("members") }]; + const actions = [ + { + title: t("moveTo"), + onClick: () => console.log("TO DO: Add move to functionality"), + }, + { + title: t("common:Delete"), + onClick: () => onDelete, + }, + ]; - console.log(list); return ( - - - -
+ + {formattedData && ( + + + +
+ )} +
); }; diff --git a/src/groups/GroupsSection.css b/src/groups/GroupsSection.css new file mode 100644 index 0000000000..41eb77c6cb --- /dev/null +++ b/src/groups/GroupsSection.css @@ -0,0 +1,10 @@ +.keycloak-admin--groups__member-count { + display: flex; + align-items: center; +} + +.keycloak-admin--groups__member-count svg { + color: var(--pf-global--Color--200); + font-size: var(--pf-global--FontSize--md); + margin-right: var(--pf-global--spacer--sm); +} diff --git a/src/groups/GroupsSection.tsx b/src/groups/GroupsSection.tsx index c49c029de1..4a5108e297 100644 --- a/src/groups/GroupsSection.tsx +++ b/src/groups/GroupsSection.tsx @@ -1,55 +1,144 @@ -import React, { useContext, useState } from "react"; +import React, { useContext, useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useHistory } from "react-router-dom"; -import { Button, PageSection } from "@patternfly/react-core"; - import { HttpClientContext } from "../http-service/HttpClientContext"; import { GroupsList } from "./GroupsList"; -import { DataLoader } from "../components/data-loader/DataLoader"; import { GroupRepresentation } from "./models/groups"; +import { + ServerGroupsArrayRepresentation, + ServerGroupMembersRepresentation, +} from "./models/server-info"; import { TableToolbar } from "../components/table-toolbar/TableToolbar"; +import { + Button, + Divider, + Dropdown, + DropdownItem, + KebabToggle, + PageSection, + PageSectionVariants, + Title, + TitleSizes, + ToolbarItem, +} from "@patternfly/react-core"; +import "./GroupsSection.css"; export const GroupsSection = () => { const { t } = useTranslation("groups"); - const history = useHistory(); const httpClient = useContext(HttpClientContext)!; + const [rawData, setRawData] = useState([{}]); + const [filteredData, setFilteredData] = useState([{}]); const [max, setMax] = useState(10); const [first, setFirst] = useState(0); + const [isKebabOpen, setIsKebabOpen] = useState(false); + const columnID: keyof GroupRepresentation = "id"; + const membersLength: keyof GroupRepresentation = "membersLength"; + const columnGroupName: keyof GroupRepresentation = "name"; const loader = async () => { - return await httpClient - .doGet("/admin/realms/master/groups", { params: { first, max } }) - .then((r) => r.data as GroupRepresentation[]); + const groups = await httpClient.doGet( + "/admin/realms/master/groups", + { params: { first, max } } + ); + const groupsData = groups.data!; + + const getMembers = async (id: number) => { + const response = await httpClient.doGet< + ServerGroupMembersRepresentation[] + >(`/admin/realms/master/groups/${id}/members`); + const responseData = response.data!; + return responseData.length; + }; + + const memberPromises = groupsData.map((group: { [key: string]: any }) => + getMembers(group[columnID]) + ); + const memberData = await Promise.all(memberPromises); + const updatedObject = groupsData.map( + (group: { [key: string]: any }, i: number) => { + const object = Object.assign({}, group); + object[membersLength] = memberData[i]; + return object; + } + ); + return updatedObject; + }; + + useEffect(() => { + loader().then((data: GroupRepresentation[]) => { + data && setRawData(data); + setFilteredData(data); + }); + }, []); + + // Filter groups + const filterGroups = (newInput: string) => { + const localRowData: object[] = []; + rawData.forEach(function (obj: { [key: string]: string }) { + const groupName = obj[columnGroupName]; + if (groupName.toLowerCase().includes(newInput.toLowerCase())) { + localRowData.push(obj); + } + }); + setFilteredData(localRowData); + }; + + // Kebab delete action + const onKebabToggle = (isOpen: boolean) => { + setIsKebabOpen(isOpen); + }; + + const onKebabSelect = () => { + setIsKebabOpen(!isKebabOpen); }; return ( - <> - - - {(groups) => ( - { - setFirst(f); - setMax(m); - }} - toolbarItem={ - <> - - - } - > - - - )} - + + + + {t("groups")} + - + + + { + setFirst(f); + setMax(m); + }} + inputGroupName="groupsToolbarTextInput" + inputGroupPlaceholder="Search groups" + inputGroupOnChange={filterGroups} + toolbarItem={ + <> + + + + + } + isOpen={isKebabOpen} + isPlain + dropdownItems={[ + + {t("delete")} + , + ]} + /> + + + } + > + {rawData && filteredData && ( + + )} + + + ); }; diff --git a/src/groups/__tests__/mock-groups.json b/src/groups/__tests__/mock-groups.json new file mode 100644 index 0000000000..1007b0675a --- /dev/null +++ b/src/groups/__tests__/mock-groups.json @@ -0,0 +1,26 @@ +[ + { + "name": "IT-1", + "groupNumber": 732 + }, + { + "name": "IT-2", + "groupNumber": 532 + }, + { + "name": "IT-3", + "groupNumber": 43 + }, + { + "name": "3scale-group", + "groupNumber": 732 + }, + { + "name": "Fuse-group", + "groupNumber": 532 + }, + { + "name": "Apicurio-group", + "groupNumber": 43 + } +] diff --git a/src/groups/messages.json b/src/groups/messages.json new file mode 100644 index 0000000000..278e38d2e2 --- /dev/null +++ b/src/groups/messages.json @@ -0,0 +1,14 @@ +{ + "groups": { + "groups": "Groups", + "createGroup": "Create group", + "groupName": "Group name", + "searchForGroups": "Search for groups", + "searchGroups": "Search groups", + "search": "Search", + "members": "Members", + "moveTo": "Move to", + "delete": "Delete", + "tableOfGroups": "Table of groups" + } +} diff --git a/src/groups/models/groups.tsx b/src/groups/models/groups.ts similarity index 81% rename from src/groups/models/groups.tsx rename to src/groups/models/groups.ts index 63127542b5..3a49f075a1 100644 --- a/src/groups/models/groups.tsx +++ b/src/groups/models/groups.ts @@ -7,4 +7,7 @@ export interface GroupRepresentation { clientRoles?: { [index: string]: string[] }; subGroups?: GroupRepresentation[]; access?: { [index: string]: boolean }; + groupNumber?: number; + membersLength?: number; + list?: []; } diff --git a/src/groups/models/server-info.ts b/src/groups/models/server-info.ts new file mode 100644 index 0000000000..ad5871db40 --- /dev/null +++ b/src/groups/models/server-info.ts @@ -0,0 +1,15 @@ +export interface ServerGroupsRepresentation { + id?: number; + name?: string; + path?: string; + subGroups?: []; +} + +// TO DO: Update this to represent the data that is returned +export interface ServerGroupMembersRepresentation { + data?: []; +} + +export interface ServerGroupsArrayRepresentation { + groups: { [index: string]: ServerGroupsRepresentation[] }; +} diff --git a/src/i18n.ts b/src/i18n.ts index bea311de56..e8f0a23f01 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -5,6 +5,7 @@ import { initReactI18next } from "react-i18next"; import common from "./common-messages.json"; import clients from "./clients/messages.json"; import clientScopes from "./client-scopes/messages.json"; +import groups from "./groups/messages.json"; import realm from "./realm/messages.json"; import roles from "./realm-roles/messages.json"; import help from "./help.json"; @@ -12,7 +13,15 @@ import help from "./help.json"; const initOptions = { defaultNS: "common", resources: { - en: { ...common, ...help, ...clients, ...clientScopes, ...realm, ...roles }, + en: { + ...common, + ...help, + ...clients, + ...clientScopes, + ...groups, + ...realm, + ...roles, + }, }, lng: "en", fallbackLng: "en", diff --git a/src/realm-roles/__tests__/mock-roles.json b/src/realm-roles/__tests__/mock-roles.json index e98cefd363..556e4c6a1d 100644 --- a/src/realm-roles/__tests__/mock-roles.json +++ b/src/realm-roles/__tests__/mock-roles.json @@ -1,77 +1,77 @@ [ - { - "name":"Admin", - "composite":true, - "description": "Lorem ipsum dolor sit amet" - }, - { - "name":"Author", - "composite":false, - "description": "Lorem ipsum dolor sit amet" - }, - { - "name":"Billing", - "composite":true, - "description": "Lorem ipsum dolor sit" - }, - { - "name":"Contributor", - "composite":true, - "description": "Lorem ipsum dolor sit, consecte" - }, - { - "name":"Editor", - "composite":true, - "description": "Lorem ipsum dolor sit amet" - }, - { - "name":"Engineer", - "composite":true, - "description": "Lorem ipsum dolor sit amet" - }, - { - "name":"Member", - "composite":false, - "description": "Lorem ipsum dolor sit amet" - }, - { - "name":"Moderator", - "composite":true, - "description": "Lorem ipsum dolor sit amet" - }, - { - "name":"Owner", - "composite":true, - "description": "Lorem ipsum dolor sit amet" - }, - { - "name":"Reader", - "composite":true, - "description": "Lorem ipsum dolor sit amet" - }, - { - "name":"Subscriber", - "composite":true, - "description": "Lorem ipsum dolor sit " - }, - { - "name":"Teenager", - "composite":true, - "description": "Lorem ipsum dolor sit amet, consecte occaecat" - }, - { - "name":"User", - "composite":true, - "description": "Lorem ipsum dolor sit amet, consecte" - }, - { - "name":"Writer", - "composite":true, - "description": "Lorem ipsum dolor" - }, - { - "name":"Zara", - "composite":true, - "description": "Lorem ipsum dolor sit amet" - } - ] \ No newline at end of file + { + "name":"Admin", + "composite":true, + "description": "Lorem ipsum dolor sit amet" + }, + { + "name":"Author", + "composite":false, + "description": "Lorem ipsum dolor sit amet" + }, + { + "name":"Billing", + "composite":true, + "description": "Lorem ipsum dolor sit" + }, + { + "name":"Contributor", + "composite":true, + "description": "Lorem ipsum dolor sit, consecte" + }, + { + "name":"Editor", + "composite":true, + "description": "Lorem ipsum dolor sit amet" + }, + { + "name":"Engineer", + "composite":true, + "description": "Lorem ipsum dolor sit amet" + }, + { + "name":"Member", + "composite":false, + "description": "Lorem ipsum dolor sit amet" + }, + { + "name":"Moderator", + "composite":true, + "description": "Lorem ipsum dolor sit amet" + }, + { + "name":"Owner", + "composite":true, + "description": "Lorem ipsum dolor sit amet" + }, + { + "name":"Reader", + "composite":true, + "description": "Lorem ipsum dolor sit amet" + }, + { + "name":"Subscriber", + "composite":true, + "description": "Lorem ipsum dolor sit " + }, + { + "name":"Teenager", + "composite":true, + "description": "Lorem ipsum dolor sit amet, consecte occaecat" + }, + { + "name":"User", + "composite":true, + "description": "Lorem ipsum dolor sit amet, consecte" + }, + { + "name":"Writer", + "composite":true, + "description": "Lorem ipsum dolor" + }, + { + "name":"Zara", + "composite":true, + "description": "Lorem ipsum dolor sit amet" + } +] \ No newline at end of file diff --git a/src/realm-roles/messages.json b/src/realm-roles/messages.json index 8e434d5d15..1f7bfeb5be 100644 --- a/src/realm-roles/messages.json +++ b/src/realm-roles/messages.json @@ -1,22 +1,21 @@ { - "roles": { - "createRole": "Create role", - "importRole": "Import role", - "roleID": "Role ID", - "type": "Type", - "homeURL": "Home URL", - "roleExplain": "Realm-level roles are a global namespace to define your roles.", - "roleName": "Role name", - "composite": "Composite", - "description": "Description", - "roleList": "Role list", - "generalSettings": "General Settings", - "capabilityConfig": "Capability config", - "roleImportError": "Could not import role", - "roleImportSuccess": "Role imported succeful", - "roleDeletedSucess": "The role has been deleted", - "roleDeleteError": "Could not delete role:", - "roleAuthentication": "Role authentication" - } + "roles": { + "createRole": "Create role", + "importRole": "Import role", + "roleID": "Role ID", + "type": "Type", + "homeURL": "Home URL", + "roleExplain": "Realm-level roles are a global namespace to define your roles.", + "roleName": "Role name", + "composite": "Composite", + "description": "Description", + "roleList": "Role list", + "generalSettings": "General Settings", + "capabilityConfig": "Capability config", + "roleImportError": "Could not import role", + "roleImportSuccess": "Role imported succeful", + "roleDeletedSucess": "The role has been deleted", + "roleDeleteError": "Could not delete role:", + "roleAuthentication": "Role authentication" } - \ No newline at end of file +} diff --git a/src/realm/messages.json b/src/realm/messages.json index 669d63f298..552d731c7f 100644 --- a/src/realm/messages.json +++ b/src/realm/messages.json @@ -9,4 +9,4 @@ "noRealmRoles": "No realm roles", "emptyStateText": "There aren't any realm roles in this realm. Create a realm role to get started." } -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 828b38a832..37d936bb01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15237,7 +15237,12 @@ prepend-http@^1.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= -prettier@^2.0.5, prettier@~2.0.5: +prettier@^2.0.5: + version "2.1.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.1.2.tgz#3050700dae2e4c8b67c4c3f666cdb8af405e1ce5" + integrity sha512-16c7K+x4qVlJg9rEbXl7HEGmQyZlG4R9AgP+oHKRMsMsuk8s+ATStlf1NpDqyBI1HpVyfjLOeMhH2LvuNvV5Vg== + +prettier@~2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.0.5.tgz#d6d56282455243f2f92cc1716692c08aa31522d4" integrity sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==