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
This commit is contained in:
Christie Molloy 2020-09-28 11:58:03 -04:00 committed by GitHub
parent 94b26936d3
commit 32f5aa0e6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 7783 additions and 7513 deletions

View file

@ -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"
}
}
}

View file

@ -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
}
}

View file

@ -47,6 +47,8 @@ export const ClientsSection = () => {
setFirst(first);
setMax(max);
}}
inputGroupName="clientsToolbarTextInput"
inputGroupPlaceholder={t("Search for client")}
toolbarItem={
<>
<Button onClick={() => history.push("/add-client")}>

File diff suppressed because it is too large Load diff

View file

@ -33,6 +33,7 @@
"rootUrl": "Root URL",
"validRedirectUri": "Valid redirect URIs",
"loginTheme": "Login theme",
"consentRequired": "Consent required"
"consentRequired": "Consent required",
"searchForClient": "Search for client"
}
}

View file

@ -7,9 +7,11 @@ import {
InputGroup,
TextInput,
Button,
ButtonVariant,
Pagination,
} from "@patternfly/react-core";
import { SearchIcon } from "@patternfly/react-icons";
import { useTranslation } from "react-i18next";
type TableToolbarProps = {
count: number;
@ -20,6 +22,12 @@ type TableToolbarProps = {
onPerPageSelect: (max: number, first: number) => void;
toolbarItem?: React.ReactNode;
children: React.ReactNode;
inputGroupName?: string;
inputGroupPlaceholder?: string;
inputGroupOnChange?: (
newInput: string,
event: React.FormEvent<HTMLInputElement>
) => void;
};
export const TableToolbar = ({
@ -31,7 +39,11 @@ export const TableToolbar = ({
onPerPageSelect,
toolbarItem,
children,
inputGroupName,
inputGroupPlaceholder,
inputGroupOnChange,
}: TableToolbarProps) => {
const { t } = useTranslation("groups");
const page = first / max;
const pagination = (variant: "top" | "bottom" = "top") => (
<Pagination
@ -55,15 +67,29 @@ export const TableToolbar = ({
<>
<Toolbar>
<ToolbarContent>
<ToolbarItem>
<InputGroup>
<TextInput type="text" aria-label="search for client criteria" />
<Button variant="control" aria-label="search for client">
<SearchIcon />
</Button>
</InputGroup>
</ToolbarItem>
{toolbarItem && <ToolbarItem>{toolbarItem}</ToolbarItem>}
<React.Fragment>
{inputGroupName && (
<ToolbarItem>
<InputGroup>
<TextInput
name={inputGroupName}
id={inputGroupName}
type="search"
aria-label={t("Search")}
placeholder={inputGroupPlaceholder}
onChange={inputGroupOnChange}
/>
<Button
variant={ButtonVariant.control}
aria-label={t("Search")}
>
<SearchIcon />
</Button>
</InputGroup>
</ToolbarItem>
)}
</React.Fragment>
{toolbarItem}
<ToolbarItem variant="pagination">{pagination()}</ToolbarItem>
</ToolbarContent>
</Toolbar>

View file

@ -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: [<Button key="0">Test</Button>], 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: [
<Button variant="link" key={index}>
{groupName}
</Button>,
<div className="keycloak-admin--groups__member-count" key={index}>
<UsersIcon />
{groupNumber}
</div>,
],
selected: false,
};
});
useEffect(() => {
setFormattedData(formatData(list!));
}, [list]);
function onSelect(
event: React.FormEvent<HTMLInputElement>,
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 (
<Table
aria-label="Simple Table"
variant={TableVariant.compact}
cells={[{ title: t("Name") }]}
rows={data}
>
<TableHeader />
<TableBody />
</Table>
<React.Fragment>
{formattedData && (
<Table
actions={actions}
variant={TableVariant.compact}
onSelect={onSelect}
canSelectAll={false}
aria-label={t("tableOfGroups")}
cells={tableHeader}
rows={formattedData}
>
<TableHeader />
<TableBody />
</Table>
)}
</React.Fragment>
);
};

View file

@ -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);
}

View file

@ -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<ServerGroupsArrayRepresentation[]>(
"/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 (
<>
<PageSection variant="light">
<DataLoader loader={loader}>
{(groups) => (
<TableToolbar
count={groups!.length}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(f, m) => {
setFirst(f);
setMax(m);
}}
toolbarItem={
<>
<Button onClick={() => history.push("/add-group")}>
{t("Create group")}
</Button>
</>
}
>
<GroupsList list={groups} />
</TableToolbar>
)}
</DataLoader>
<React.Fragment>
<PageSection variant={PageSectionVariants.light}>
<Title headingLevel="h3" size={TitleSizes["2xl"]}>
{t("groups")}
</Title>
</PageSection>
</>
<Divider />
<PageSection variant={PageSectionVariants.light}>
<TableToolbar
count={10}
first={first}
max={max}
onNextClick={setFirst}
onPreviousClick={setFirst}
onPerPageSelect={(f, m) => {
setFirst(f);
setMax(m);
}}
inputGroupName="groupsToolbarTextInput"
inputGroupPlaceholder="Search groups"
inputGroupOnChange={filterGroups}
toolbarItem={
<>
<ToolbarItem>
<Button variant="primary">{t("createGroup")}</Button>
</ToolbarItem>
<ToolbarItem>
<Dropdown
onSelect={onKebabSelect}
toggle={<KebabToggle onToggle={onKebabToggle} />}
isOpen={isKebabOpen}
isPlain
dropdownItems={[
<DropdownItem key="action" component="button">
{t("delete")}
</DropdownItem>,
]}
/>
</ToolbarItem>
</>
}
>
{rawData && filteredData && (
<GroupsList list={filteredData ? filteredData : rawData} />
)}
</TableToolbar>
</PageSection>
</React.Fragment>
);
};

View file

@ -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
}
]

14
src/groups/messages.json Normal file
View file

@ -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"
}
}

View file

@ -7,4 +7,7 @@ export interface GroupRepresentation {
clientRoles?: { [index: string]: string[] };
subGroups?: GroupRepresentation[];
access?: { [index: string]: boolean };
groupNumber?: number;
membersLength?: number;
list?: [];
}

View file

@ -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[] };
}

View file

@ -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",

View file

@ -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"
}
]
{
"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"
}
]

View file

@ -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"
}
}

View file

@ -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."
}
}
}

View file

@ -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==