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:
parent
94b26936d3
commit
32f5aa0e6f
18 changed files with 7783 additions and 7513 deletions
|
@ -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")}>
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"rootUrl": "Root URL",
|
||||
"validRedirectUri": "Valid redirect URIs",
|
||||
"loginTheme": "Login theme",
|
||||
"consentRequired": "Consent required"
|
||||
"consentRequired": "Consent required",
|
||||
"searchForClient": "Search for client"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
<React.Fragment>
|
||||
{inputGroupName && (
|
||||
<ToolbarItem>
|
||||
<InputGroup>
|
||||
<TextInput type="text" aria-label="search for client criteria" />
|
||||
<Button variant="control" aria-label="search for client">
|
||||
<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>
|
||||
{toolbarItem && <ToolbarItem>{toolbarItem}</ToolbarItem>}
|
||||
)}
|
||||
</React.Fragment>
|
||||
{toolbarItem}
|
||||
<ToolbarItem variant="pagination">{pagination()}</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
||||
|
||||
console.log(list);
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{formattedData && (
|
||||
<Table
|
||||
aria-label="Simple Table"
|
||||
actions={actions}
|
||||
variant={TableVariant.compact}
|
||||
cells={[{ title: t("Name") }]}
|
||||
rows={data}
|
||||
onSelect={onSelect}
|
||||
canSelectAll={false}
|
||||
aria-label={t("tableOfGroups")}
|
||||
cells={tableHeader}
|
||||
rows={formattedData}
|
||||
>
|
||||
<TableHeader />
|
||||
<TableBody />
|
||||
</Table>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
|
10
src/groups/GroupsSection.css
Normal file
10
src/groups/GroupsSection.css
Normal 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);
|
||||
}
|
|
@ -1,34 +1,107 @@
|
|||
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) => (
|
||||
<React.Fragment>
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<Title headingLevel="h3" size={TitleSizes["2xl"]}>
|
||||
{t("groups")}
|
||||
</Title>
|
||||
</PageSection>
|
||||
<Divider />
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<TableToolbar
|
||||
count={groups!.length}
|
||||
count={10}
|
||||
first={first}
|
||||
max={max}
|
||||
onNextClick={setFirst}
|
||||
|
@ -37,19 +110,35 @@ export const GroupsSection = () => {
|
|||
setFirst(f);
|
||||
setMax(m);
|
||||
}}
|
||||
inputGroupName="groupsToolbarTextInput"
|
||||
inputGroupPlaceholder="Search groups"
|
||||
inputGroupOnChange={filterGroups}
|
||||
toolbarItem={
|
||||
<>
|
||||
<Button onClick={() => history.push("/add-group")}>
|
||||
{t("Create group")}
|
||||
</Button>
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<GroupsList list={groups} />
|
||||
</TableToolbar>
|
||||
{rawData && filteredData && (
|
||||
<GroupsList list={filteredData ? filteredData : rawData} />
|
||||
)}
|
||||
</DataLoader>
|
||||
</TableToolbar>
|
||||
</PageSection>
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
|
26
src/groups/__tests__/mock-groups.json
Normal file
26
src/groups/__tests__/mock-groups.json
Normal 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
14
src/groups/messages.json
Normal 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"
|
||||
}
|
||||
}
|
|
@ -7,4 +7,7 @@ export interface GroupRepresentation {
|
|||
clientRoles?: { [index: string]: string[] };
|
||||
subGroups?: GroupRepresentation[];
|
||||
access?: { [index: string]: boolean };
|
||||
groupNumber?: number;
|
||||
membersLength?: number;
|
||||
list?: [];
|
||||
}
|
15
src/groups/models/server-info.ts
Normal file
15
src/groups/models/server-info.ts
Normal 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[] };
|
||||
}
|
11
src/i18n.ts
11
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",
|
||||
|
|
|
@ -74,4 +74,4 @@
|
|||
"composite":true,
|
||||
"description": "Lorem ipsum dolor sit amet"
|
||||
}
|
||||
]
|
||||
]
|
|
@ -18,5 +18,4 @@
|
|||
"roleDeleteError": "Could not delete role:",
|
||||
"roleAuthentication": "Role authentication"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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==
|
||||
|
|
Loading…
Reference in a new issue