Merge pull request #142 from christiemolloy/createGroup
Add Create Group Functionality
This commit is contained in:
commit
bcd864bfc9
4 changed files with 223 additions and 90 deletions
99
src/groups/GroupsCreateModal.tsx
Normal file
99
src/groups/GroupsCreateModal.tsx
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import React, { useContext } from "react";
|
||||||
|
import {
|
||||||
|
AlertVariant,
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormGroup,
|
||||||
|
Modal,
|
||||||
|
ModalVariant,
|
||||||
|
TextInput,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { HttpClientContext } from "../context/http-service/HttpClientContext";
|
||||||
|
import { RealmContext } from "../context/realm-context/RealmContext";
|
||||||
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
type GroupsCreateModalProps = {
|
||||||
|
handleModalToggle: () => void;
|
||||||
|
isCreateModalOpen: boolean;
|
||||||
|
setIsCreateModalOpen: (isCreateModalOpen: boolean) => void;
|
||||||
|
createGroupName: string;
|
||||||
|
setCreateGroupName: (createGroupName: string) => void;
|
||||||
|
refresh: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GroupsCreateModal = ({
|
||||||
|
handleModalToggle,
|
||||||
|
isCreateModalOpen,
|
||||||
|
setIsCreateModalOpen,
|
||||||
|
createGroupName,
|
||||||
|
setCreateGroupName,
|
||||||
|
refresh,
|
||||||
|
}: GroupsCreateModalProps) => {
|
||||||
|
const { t } = useTranslation("groups");
|
||||||
|
const httpClient = useContext(HttpClientContext)!;
|
||||||
|
const { realm } = useContext(RealmContext);
|
||||||
|
const { addAlert } = useAlerts();
|
||||||
|
const form = useForm();
|
||||||
|
const { register, errors } = form;
|
||||||
|
|
||||||
|
const valueChange = (createGroupName: string) => {
|
||||||
|
setCreateGroupName(createGroupName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = async () => {
|
||||||
|
if (await form.trigger()) {
|
||||||
|
try {
|
||||||
|
await httpClient.doPost(`/admin/realms/${realm}/groups`, {
|
||||||
|
name: createGroupName,
|
||||||
|
});
|
||||||
|
setIsCreateModalOpen(false);
|
||||||
|
setCreateGroupName("");
|
||||||
|
refresh();
|
||||||
|
addAlert(t("groupCreated"), AlertVariant.success);
|
||||||
|
} catch (error) {
|
||||||
|
addAlert(
|
||||||
|
`${t("couldNotCreateGroup")} ': '${error}'`,
|
||||||
|
AlertVariant.danger
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
variant={ModalVariant.small}
|
||||||
|
title={t("createAGroup")}
|
||||||
|
isOpen={isCreateModalOpen}
|
||||||
|
onClose={handleModalToggle}
|
||||||
|
actions={[
|
||||||
|
<Button key="confirm" variant="primary" onClick={() => submitForm()}>
|
||||||
|
{t("create")}
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Form isHorizontal>
|
||||||
|
<FormGroup
|
||||||
|
name="create-modal-group"
|
||||||
|
label={t("name")}
|
||||||
|
fieldId="group-id"
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
validated={errors.name ? "error" : "default"}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
ref={register({ required: true })}
|
||||||
|
type="text"
|
||||||
|
id="create-group-name"
|
||||||
|
name="name"
|
||||||
|
value={createGroupName}
|
||||||
|
onChange={valueChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,14 +5,17 @@ import {
|
||||||
TableBody,
|
TableBody,
|
||||||
TableVariant,
|
TableVariant,
|
||||||
} from "@patternfly/react-table";
|
} from "@patternfly/react-table";
|
||||||
import { Button } from "@patternfly/react-core";
|
import { Button, AlertVariant } from "@patternfly/react-core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { GroupRepresentation } from "./models/groups";
|
import { GroupRepresentation } from "./models/groups";
|
||||||
import { UsersIcon } from "@patternfly/react-icons";
|
import { UsersIcon } from "@patternfly/react-icons";
|
||||||
import { HttpClientContext } from "../context/http-service/HttpClientContext";
|
import { HttpClientContext } from "../context/http-service/HttpClientContext";
|
||||||
|
import { RealmContext } from "../context/realm-context/RealmContext";
|
||||||
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
|
|
||||||
type GroupsListProps = {
|
export type GroupsListProps = {
|
||||||
list?: GroupRepresentation[];
|
list?: GroupRepresentation[];
|
||||||
|
refresh: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FormattedData = {
|
type FormattedData = {
|
||||||
|
@ -20,11 +23,13 @@ type FormattedData = {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GroupsList = ({ list }: GroupsListProps) => {
|
export const GroupsList = ({ list, refresh }: GroupsListProps) => {
|
||||||
const { t } = useTranslation("groups");
|
const { t } = useTranslation("groups");
|
||||||
const httpClient = useContext(HttpClientContext)!;
|
const httpClient = useContext(HttpClientContext)!;
|
||||||
const columnGroupName: keyof GroupRepresentation = "name";
|
const columnGroupName: keyof GroupRepresentation = "name";
|
||||||
const columnGroupNumber: keyof GroupRepresentation = "membersLength";
|
const columnGroupNumber: keyof GroupRepresentation = "membersLength";
|
||||||
|
const { realm } = useContext(RealmContext);
|
||||||
|
const { addAlert } = useAlerts();
|
||||||
const [formattedData, setFormattedData] = useState<FormattedData[]>([]);
|
const [formattedData, setFormattedData] = useState<FormattedData[]>([]);
|
||||||
|
|
||||||
const formatData = (data: GroupRepresentation[]) =>
|
const formatData = (data: GroupRepresentation[]) =>
|
||||||
|
@ -67,15 +72,6 @@ export const GroupsList = ({ list }: GroupsListProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 tableHeader = [{ title: t("groupName") }, { title: t("members") }];
|
||||||
const actions = [
|
const actions = [
|
||||||
{
|
{
|
||||||
|
@ -84,7 +80,20 @@ export const GroupsList = ({ list }: GroupsListProps) => {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("common:Delete"),
|
title: t("common:Delete"),
|
||||||
onClick: () => onDelete,
|
onClick: async (
|
||||||
|
_: React.MouseEvent<Element, MouseEvent>,
|
||||||
|
rowId: number
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await httpClient.doDelete(
|
||||||
|
`/admin/realms/${realm}/groups/${list![rowId].id}`
|
||||||
|
);
|
||||||
|
refresh();
|
||||||
|
addAlert(t("Group deleted"), AlertVariant.success);
|
||||||
|
} catch (error) {
|
||||||
|
addAlert(`${t("clientDeleteError")} ${error}`, AlertVariant.danger);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -2,24 +2,23 @@ import React, { useContext, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { HttpClientContext } from "../context/http-service/HttpClientContext";
|
import { HttpClientContext } from "../context/http-service/HttpClientContext";
|
||||||
import { GroupsList } from "./GroupsList";
|
import { GroupsList } from "./GroupsList";
|
||||||
|
import { GroupsCreateModal } from "./GroupsCreateModal";
|
||||||
import { GroupRepresentation } from "./models/groups";
|
import { GroupRepresentation } from "./models/groups";
|
||||||
import {
|
import {
|
||||||
ServerGroupsArrayRepresentation,
|
ServerGroupsArrayRepresentation,
|
||||||
ServerGroupMembersRepresentation,
|
ServerGroupMembersRepresentation,
|
||||||
} from "./models/server-info";
|
} from "./models/server-info";
|
||||||
import { TableToolbar } from "../components/table-toolbar/TableToolbar";
|
import { TableToolbar } from "../components/table-toolbar/TableToolbar";
|
||||||
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Divider,
|
|
||||||
Dropdown,
|
Dropdown,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
KebabToggle,
|
KebabToggle,
|
||||||
PageSection,
|
PageSection,
|
||||||
PageSectionVariants,
|
PageSectionVariants,
|
||||||
Spinner,
|
Spinner,
|
||||||
Title,
|
|
||||||
TitleSizes,
|
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import "./GroupsSection.css";
|
import "./GroupsSection.css";
|
||||||
|
@ -30,39 +29,43 @@ export const GroupsSection = () => {
|
||||||
const [rawData, setRawData] = useState<{ [key: string]: any }[]>();
|
const [rawData, setRawData] = useState<{ [key: string]: any }[]>();
|
||||||
const [filteredData, setFilteredData] = useState<object[]>();
|
const [filteredData, setFilteredData] = useState<object[]>();
|
||||||
const [isKebabOpen, setIsKebabOpen] = useState(false);
|
const [isKebabOpen, setIsKebabOpen] = useState(false);
|
||||||
|
const [createGroupName, setCreateGroupName] = useState("");
|
||||||
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
const columnID: keyof GroupRepresentation = "id";
|
const columnID: keyof GroupRepresentation = "id";
|
||||||
const membersLength: keyof GroupRepresentation = "membersLength";
|
const membersLength: keyof GroupRepresentation = "membersLength";
|
||||||
const columnGroupName: keyof GroupRepresentation = "name";
|
const columnGroupName: keyof GroupRepresentation = "name";
|
||||||
|
|
||||||
|
const loader = async () => {
|
||||||
|
const groups = await httpClient.doGet<ServerGroupsArrayRepresentation[]>(
|
||||||
|
"/admin/realms/master/groups"
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setRawData(updatedObject);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
loader();
|
||||||
const groups = await httpClient.doGet<ServerGroupsArrayRepresentation[]>(
|
|
||||||
"/admin/realms/master/groups"
|
|
||||||
);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
setRawData(updatedObject);
|
|
||||||
})();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Filter groups
|
// Filter groups
|
||||||
|
@ -83,15 +86,13 @@ export const GroupsSection = () => {
|
||||||
setIsKebabOpen(!isKebabOpen);
|
setIsKebabOpen(!isKebabOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const handleModalToggle = () => {
|
||||||
<React.Fragment>
|
setIsCreateModalOpen(!isCreateModalOpen);
|
||||||
<PageSection variant={PageSectionVariants.light}>
|
};
|
||||||
<Title headingLevel="h3" size={TitleSizes["2xl"]}>
|
|
||||||
{t("groups")}
|
|
||||||
</Title>
|
|
||||||
</PageSection>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewHeader titleKey="groups:groups" subKey="groups:groupsDescription" />
|
||||||
<PageSection variant={PageSectionVariants.light}>
|
<PageSection variant={PageSectionVariants.light}>
|
||||||
{!rawData && (
|
{!rawData && (
|
||||||
<div className="pf-u-text-align-center">
|
<div className="pf-u-text-align-center">
|
||||||
|
@ -99,43 +100,61 @@ export const GroupsSection = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{rawData && rawData.length > 0 ? (
|
{rawData && rawData.length > 0 ? (
|
||||||
<TableToolbar
|
<>
|
||||||
inputGroupName="groupsToolbarTextInput"
|
<TableToolbar
|
||||||
inputGroupPlaceholder={t("searchGroups")}
|
inputGroupName="groupsToolbarTextInput"
|
||||||
inputGroupOnChange={filterGroups}
|
inputGroupPlaceholder={t("searchGroups")}
|
||||||
toolbarItem={
|
inputGroupOnChange={filterGroups}
|
||||||
<>
|
toolbarItem={
|
||||||
<ToolbarItem>
|
<>
|
||||||
<Button variant="primary">{t("createGroup")}</Button>
|
<ToolbarItem>
|
||||||
</ToolbarItem>
|
<Button
|
||||||
<ToolbarItem>
|
variant="primary"
|
||||||
<Dropdown
|
onClick={() => handleModalToggle()}
|
||||||
onSelect={onKebabSelect}
|
>
|
||||||
toggle={<KebabToggle onToggle={onKebabToggle} />}
|
{t("createGroup")}
|
||||||
isOpen={isKebabOpen}
|
</Button>
|
||||||
isPlain
|
</ToolbarItem>
|
||||||
dropdownItems={[
|
<ToolbarItem>
|
||||||
<DropdownItem key="action" component="button">
|
<Dropdown
|
||||||
{t("delete")}
|
onSelect={onKebabSelect}
|
||||||
</DropdownItem>,
|
toggle={<KebabToggle onToggle={onKebabToggle} />}
|
||||||
]}
|
isOpen={isKebabOpen}
|
||||||
/>
|
isPlain
|
||||||
</ToolbarItem>
|
dropdownItems={[
|
||||||
</>
|
<DropdownItem key="action" component="button">
|
||||||
}
|
{t("delete")}
|
||||||
>
|
</DropdownItem>,
|
||||||
{rawData && (
|
]}
|
||||||
<GroupsList list={filteredData ? filteredData : rawData} />
|
/>
|
||||||
)}
|
</ToolbarItem>
|
||||||
{filteredData && filteredData.length === 0 && (
|
</>
|
||||||
<ListEmptyState
|
}
|
||||||
hasIcon={true}
|
>
|
||||||
isSearchVariant={true}
|
{rawData && (
|
||||||
message={t("noSearchResults")}
|
<GroupsList
|
||||||
instructions={t("noSearchResultsInstructions")}
|
list={filteredData ? filteredData : rawData}
|
||||||
/>
|
refresh={loader}
|
||||||
)}
|
/>
|
||||||
</TableToolbar>
|
)}
|
||||||
|
{filteredData && filteredData.length === 0 && (
|
||||||
|
<ListEmptyState
|
||||||
|
hasIcon={true}
|
||||||
|
isSearchVariant={true}
|
||||||
|
message={t("noSearchResults")}
|
||||||
|
instructions={t("noSearchResultsInstructions")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableToolbar>
|
||||||
|
<GroupsCreateModal
|
||||||
|
isCreateModalOpen={isCreateModalOpen}
|
||||||
|
handleModalToggle={handleModalToggle}
|
||||||
|
setIsCreateModalOpen={setIsCreateModalOpen}
|
||||||
|
createGroupName={createGroupName}
|
||||||
|
setCreateGroupName={setCreateGroupName}
|
||||||
|
refresh={loader}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<ListEmptyState
|
<ListEmptyState
|
||||||
hasIcon={true}
|
hasIcon={true}
|
||||||
|
@ -145,6 +164,6 @@ export const GroupsSection = () => {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</React.Fragment>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,6 +10,12 @@
|
||||||
"moveTo": "Move to",
|
"moveTo": "Move to",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"tableOfGroups": "Table of groups",
|
"tableOfGroups": "Table of groups",
|
||||||
|
"name": "Name",
|
||||||
|
"groupsDescription": "Description goes here",
|
||||||
|
"groupCreated": "Group created",
|
||||||
|
"couldNotCreateGroup": "Could not create group",
|
||||||
|
"createAGroup": "Create a group",
|
||||||
|
"create": "Create",
|
||||||
"noSearchResults": "No search results",
|
"noSearchResults": "No search results",
|
||||||
"noSearchResultsInstructions" : "Click on the search bar above to search for groups",
|
"noSearchResultsInstructions" : "Click on the search bar above to search for groups",
|
||||||
"noGroupsInThisRealm" : "No groups in this Realm",
|
"noGroupsInThisRealm" : "No groups in this Realm",
|
||||||
|
|
Loading…
Reference in a new issue