Search and create sub groups (#387)
* fixed group section * simplified create group dialog * create subgroup * initial search groups * added initial search * add empty state and links to details * Added cypress tests * fixed types * changed to the more clear getId * changed to use testid * fixed merge error * fixed test * changed text for empty sub groups * fix merge error * fix test
This commit is contained in:
parent
bfa0c6e1ea
commit
a48088765a
18 changed files with 434 additions and 105 deletions
53
cypress/integration/group_test.spec.ts
Normal file
53
cypress/integration/group_test.spec.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import ListingPage from "../support/pages/admin_console/ListingPage";
|
||||
import CreateGroupModal from "../support/pages/admin_console/manage/groups/CreateGroupModal";
|
||||
import { SearchGroupPage } from "../support/pages/admin_console/manage/groups/SearchGroup";
|
||||
import Masthead from "../support/pages/admin_console/Masthead";
|
||||
import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
||||
import LoginPage from "../support/pages/LoginPage";
|
||||
import ViewHeaderPage from "../support/pages/ViewHeaderPage";
|
||||
|
||||
describe("Group test", () => {
|
||||
const loginPage = new LoginPage();
|
||||
const masthead = new Masthead();
|
||||
const sidebarPage = new SidebarPage();
|
||||
const listingPage = new ListingPage();
|
||||
const viewHeaderPage = new ViewHeaderPage();
|
||||
const createGroupModal = new CreateGroupModal();
|
||||
|
||||
let groupName = "group";
|
||||
|
||||
describe("Group creation", () => {
|
||||
beforeEach(function () {
|
||||
cy.visit("");
|
||||
loginPage.logIn();
|
||||
sidebarPage.goToGroups();
|
||||
});
|
||||
|
||||
it("Group CRUD test", () => {
|
||||
groupName += "_" + (Math.random() + 1).toString(36).substring(7);
|
||||
|
||||
createGroupModal
|
||||
.open("empty-primary-action")
|
||||
.fillGroupForm(groupName)
|
||||
.clickCreate();
|
||||
|
||||
masthead.checkNotificationMessage("Group created");
|
||||
|
||||
sidebarPage.goToGroups();
|
||||
listingPage.searchItem(groupName).itemExist(groupName);
|
||||
|
||||
// Delete
|
||||
listingPage.deleteItem(groupName);
|
||||
masthead.checkNotificationMessage("Group deleted");
|
||||
|
||||
listingPage.itemExist(groupName, false);
|
||||
});
|
||||
|
||||
const searchGroupPage = new SearchGroupPage();
|
||||
it("Group search", () => {
|
||||
viewHeaderPage.clickAction("searchGroup");
|
||||
searchGroupPage.searchGroup("group").clickSearchButton();
|
||||
searchGroupPage.checkTerm("group");
|
||||
});
|
||||
});
|
||||
});
|
|
@ -214,7 +214,7 @@ describe("User Fed Kerberos tests", () => {
|
|||
.click();
|
||||
cy.wait(1000);
|
||||
|
||||
cy.get('[data-cy="action-dropdown"]').click();
|
||||
cy.get('[data-testid="action-dropdown"]').click();
|
||||
cy.get('[data-cy="delete-provider-cmd"]').click();
|
||||
|
||||
modalUtils.checkModalTitle(deleteModalTitle).confirmModal();
|
||||
|
|
|
@ -23,3 +23,7 @@
|
|||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
|
||||
Cypress.Commands.add("getId", (selector, ...args) => {
|
||||
return cy.get(`[data-testid=${selector}]`, ...args);
|
||||
});
|
||||
|
|
8
cypress/support/pages/ViewHeaderPage.ts
Normal file
8
cypress/support/pages/ViewHeaderPage.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export default class ListingPage {
|
||||
private actionMenu = "action-dropdown";
|
||||
|
||||
clickAction(action: string) {
|
||||
cy.getId(this.actionMenu).click().getId(action).click();
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
export default class CreateGroupModal {
|
||||
private openButton = "openCreateGroupModal";
|
||||
private nameInput = "groupNameInput";
|
||||
private createButton = "createGroup";
|
||||
|
||||
open(name?: string) {
|
||||
cy.getId(name || this.openButton).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
fillGroupForm(name = "") {
|
||||
cy.getId(this.nameInput).clear();
|
||||
cy.getId(this.nameInput).type(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
clickCreate() {
|
||||
cy.getId(this.createButton).click();
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
export class SearchGroupPage {
|
||||
private searchField = "group-search";
|
||||
private searchButton = "search-button";
|
||||
|
||||
searchGroup(search: string) {
|
||||
cy.getId(this.searchField).type(search);
|
||||
return this;
|
||||
}
|
||||
|
||||
clickSearchButton() {
|
||||
cy.getId(this.searchButton).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
checkTerm(searchTerm: string) {
|
||||
cy.get(".pf-c-chip-group").children().contains(searchTerm).should("exist");
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -31,7 +31,7 @@ export const ListEmptyState = ({
|
|||
message,
|
||||
instructions,
|
||||
onPrimaryAction,
|
||||
hasIcon,
|
||||
hasIcon = true,
|
||||
isSearchVariant,
|
||||
primaryActionText,
|
||||
secondaryActions,
|
||||
|
@ -42,14 +42,18 @@ export const ListEmptyState = ({
|
|||
{hasIcon && isSearchVariant ? (
|
||||
<EmptyStateIcon icon={SearchIcon} />
|
||||
) : (
|
||||
<EmptyStateIcon icon={PlusCircleIcon} />
|
||||
hasIcon && <EmptyStateIcon icon={PlusCircleIcon} />
|
||||
)}
|
||||
<Title headingLevel="h4" size="lg">
|
||||
{message}
|
||||
</Title>
|
||||
<EmptyStateBody>{instructions}</EmptyStateBody>
|
||||
{primaryActionText && (
|
||||
<Button variant="primary" onClick={onPrimaryAction}>
|
||||
<Button
|
||||
data-testid="empty-primary-action"
|
||||
variant="primary"
|
||||
onClick={onPrimaryAction}
|
||||
>
|
||||
{primaryActionText}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -38,6 +38,7 @@ exports[`<ListEmptyState /> render 1`] = `
|
|||
data-ouia-component-id="OUIA-Generated-Button-primary-1"
|
||||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
data-testid="empty-primary-action"
|
||||
type="button"
|
||||
>
|
||||
Add it now!
|
||||
|
|
|
@ -243,7 +243,7 @@ export function KeycloakDataTable<T>({
|
|||
return (
|
||||
<>
|
||||
{!rows && <Loading />}
|
||||
{rows && isPaginated && (
|
||||
{rows && (rows.length > 0 || !emptyState) && isPaginated && (
|
||||
<PaginatingTableToolbar
|
||||
count={rows.length}
|
||||
first={first}
|
||||
|
@ -263,22 +263,19 @@ export function KeycloakDataTable<T>({
|
|||
searchTypeComponent={searchTypeComponent}
|
||||
toolbarItem={toolbarItem}
|
||||
>
|
||||
{!loading && (emptyState === undefined || rows.length !== 0) && (
|
||||
<DataTable
|
||||
canSelectAll={canSelectAll}
|
||||
onSelect={onSelect ? _onSelect : undefined}
|
||||
actions={convertAction()}
|
||||
actionResolver={actionResolver}
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
ariaLabelKey={ariaLabelKey}
|
||||
/>
|
||||
)}
|
||||
{!loading && rows.length === 0 && emptyState}
|
||||
<DataTable
|
||||
canSelectAll={canSelectAll}
|
||||
onSelect={onSelect ? _onSelect : undefined}
|
||||
actions={convertAction()}
|
||||
actionResolver={actionResolver}
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
ariaLabelKey={ariaLabelKey}
|
||||
/>
|
||||
{loading && <Loading />}
|
||||
</PaginatingTableToolbar>
|
||||
)}
|
||||
{rows && !isPaginated && (
|
||||
{rows && (rows.length > 0 || !emptyState) && !isPaginated && (
|
||||
<TableToolbar
|
||||
inputGroupName={
|
||||
searchPlaceholderKey ? `${ariaLabelKey}input` : undefined
|
||||
|
@ -289,20 +286,18 @@ export function KeycloakDataTable<T>({
|
|||
toolbarItem={toolbarItem}
|
||||
searchTypeComponent={searchTypeComponent}
|
||||
>
|
||||
{(emptyState === undefined || rows.length !== 0) && (
|
||||
<DataTable
|
||||
canSelectAll={canSelectAll}
|
||||
onSelect={onSelect ? _onSelect : undefined}
|
||||
actions={convertAction()}
|
||||
actionResolver={actionResolver}
|
||||
rows={filteredData || rows}
|
||||
columns={columns}
|
||||
ariaLabelKey={ariaLabelKey}
|
||||
/>
|
||||
)}
|
||||
{rows.length === 0 && emptyState}
|
||||
<DataTable
|
||||
canSelectAll={canSelectAll}
|
||||
onSelect={onSelect ? _onSelect : undefined}
|
||||
actions={convertAction()}
|
||||
actionResolver={actionResolver}
|
||||
rows={filteredData || rows}
|
||||
columns={columns}
|
||||
ariaLabelKey={ariaLabelKey}
|
||||
/>
|
||||
</TableToolbar>
|
||||
)}
|
||||
<>{!loading && rows?.length === 0 && emptyState}</>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -59,6 +59,9 @@ export const PaginatingTableToolbar = ({
|
|||
/>
|
||||
);
|
||||
|
||||
if (count === 0) {
|
||||
<>{children}</>;
|
||||
}
|
||||
return (
|
||||
<TableToolbar
|
||||
searchTypeComponent={searchTypeComponent}
|
||||
|
|
|
@ -51,21 +51,25 @@ export const TableToolbar = ({
|
|||
<ToolbarItem>
|
||||
<InputGroup>
|
||||
{searchTypeComponent}
|
||||
<TextInput
|
||||
name={inputGroupName}
|
||||
id={inputGroupName}
|
||||
type="search"
|
||||
aria-label={t("search")}
|
||||
placeholder={inputGroupPlaceholder}
|
||||
onChange={inputGroupOnChange}
|
||||
/>
|
||||
<Button
|
||||
variant={ButtonVariant.control}
|
||||
aria-label={t("search")}
|
||||
onClick={inputGroupOnClick}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
{inputGroupPlaceholder && (
|
||||
<>
|
||||
<TextInput
|
||||
name={inputGroupName}
|
||||
id={inputGroupName}
|
||||
type="search"
|
||||
aria-label={t("search")}
|
||||
placeholder={inputGroupPlaceholder}
|
||||
onChange={inputGroupOnChange}
|
||||
/>
|
||||
<Button
|
||||
variant={ButtonVariant.control}
|
||||
aria-label={t("search")}
|
||||
onClick={inputGroupOnClick}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</InputGroup>
|
||||
</ToolbarItem>
|
||||
)}
|
||||
|
|
|
@ -118,7 +118,7 @@ export const ViewHeader = ({
|
|||
}
|
||||
isOpen={isDropdownOpen}
|
||||
dropdownItems={dropdownItems}
|
||||
data-cy="action-dropdown"
|
||||
data-testid="action-dropdown"
|
||||
/>
|
||||
</ToolbarItem>
|
||||
)}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { FormEvent } from "react";
|
||||
import React from "react";
|
||||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
|
@ -10,52 +10,41 @@ import {
|
|||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
|
||||
type GroupsCreateModalProps = {
|
||||
id?: string;
|
||||
handleModalToggle: () => void;
|
||||
isCreateModalOpen: boolean;
|
||||
setIsCreateModalOpen: (isCreateModalOpen: boolean) => void;
|
||||
createGroupName: string;
|
||||
setCreateGroupName: (createGroupName: string) => void;
|
||||
refresh: () => void;
|
||||
};
|
||||
|
||||
export const GroupsCreateModal = ({
|
||||
id,
|
||||
handleModalToggle,
|
||||
isCreateModalOpen,
|
||||
setIsCreateModalOpen,
|
||||
createGroupName,
|
||||
setCreateGroupName,
|
||||
refresh,
|
||||
}: GroupsCreateModalProps) => {
|
||||
const { t } = useTranslation("groups");
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert } = useAlerts();
|
||||
const form = useForm();
|
||||
const { register, errors } = form;
|
||||
const { register, errors, handleSubmit } = useForm();
|
||||
|
||||
const valueChange = (createGroupName: string) => {
|
||||
setCreateGroupName(createGroupName);
|
||||
};
|
||||
|
||||
const submitForm = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (await form.trigger()) {
|
||||
try {
|
||||
await adminClient.groups.create({ name: createGroupName });
|
||||
refresh();
|
||||
setIsCreateModalOpen(false);
|
||||
setCreateGroupName("");
|
||||
addAlert(t("groupCreated"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
`${t("couldNotCreateGroup")} ': '${error}'`,
|
||||
AlertVariant.danger
|
||||
);
|
||||
const submitForm = async (group: GroupRepresentation) => {
|
||||
try {
|
||||
if (!id) {
|
||||
await adminClient.groups.create({ name: group.name });
|
||||
} else {
|
||||
await adminClient.groups.setOrCreateChild({ id }, { name: group.name });
|
||||
}
|
||||
|
||||
refresh();
|
||||
handleModalToggle();
|
||||
addAlert(t("groupCreated"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(t("couldNotCreateGroup", { error }), AlertVariant.danger);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -64,15 +53,21 @@ export const GroupsCreateModal = ({
|
|||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={t("createAGroup")}
|
||||
isOpen={isCreateModalOpen}
|
||||
isOpen={true}
|
||||
onClose={handleModalToggle}
|
||||
actions={[
|
||||
<Button key="confirm" variant="primary" onClick={submitForm}>
|
||||
<Button
|
||||
data-testid="createGroup"
|
||||
key="confirm"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
form="group-form"
|
||||
>
|
||||
{t("create")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form isHorizontal onSubmit={submitForm}>
|
||||
<Form id="group-form" isHorizontal onSubmit={handleSubmit(submitForm)}>
|
||||
<FormGroup
|
||||
name="create-modal-group"
|
||||
label={t("common:name")}
|
||||
|
@ -84,13 +79,12 @@ export const GroupsCreateModal = ({
|
|||
isRequired
|
||||
>
|
||||
<TextInput
|
||||
data-testid="groupNameInput"
|
||||
ref={register({ required: true })}
|
||||
autoFocus
|
||||
type="text"
|
||||
id="create-group-name"
|
||||
name="name"
|
||||
value={createGroupName}
|
||||
onChange={valueChange}
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
|
|
|
@ -84,7 +84,6 @@ export const GroupsSection = () => {
|
|||
const { t } = useTranslation("groups");
|
||||
const adminClient = useAdminClient();
|
||||
const [isKebabOpen, setIsKebabOpen] = useState(false);
|
||||
const [createGroupName, setCreateGroupName] = useState("");
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<GroupRepresentation[]>([]);
|
||||
const { subGroups, setSubGroups } = useSubGroups();
|
||||
|
@ -189,7 +188,33 @@ export const GroupsSection = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader titleKey="groups:groups" subKey="groups:groupsDescription" />
|
||||
<ViewHeader
|
||||
titleKey="groups:groups"
|
||||
subKey="groups:groupsDescription"
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
data-testid="searchGroup"
|
||||
key="searchGroup"
|
||||
onClick={() => history.push(`/${realm}/groups/search`)}
|
||||
>
|
||||
{t("searchGroup")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
data-testid="renameGroup"
|
||||
key="renameGroup"
|
||||
onClick={() => addAlert("Not implemented")}
|
||||
>
|
||||
{t("renameGroup")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
data-testid="deleteGroup"
|
||||
key="deleteGroup"
|
||||
onClick={() => deleteGroup({ id })}
|
||||
>
|
||||
{t("deleteGroup")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
/>
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
|
@ -201,7 +226,11 @@ export const GroupsSection = () => {
|
|||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button variant="primary" onClick={handleModalToggle}>
|
||||
<Button
|
||||
data-testid="openCreateGroupModal"
|
||||
variant="primary"
|
||||
onClick={handleModalToggle}
|
||||
>
|
||||
{t("createGroup")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
|
@ -257,22 +286,23 @@ export const GroupsSection = () => {
|
|||
emptyState={
|
||||
<ListEmptyState
|
||||
hasIcon={true}
|
||||
message={t("noGroupsInThisRealm")}
|
||||
instructions={t("noGroupsInThisRealmInstructions")}
|
||||
message={t(`noGroupsInThis${id ? "SubGroup" : "Realm"}`)}
|
||||
instructions={t(
|
||||
`noGroupsInThis${id ? "SubGroup" : "Realm"}Instructions`
|
||||
)}
|
||||
primaryActionText={t("createGroup")}
|
||||
onPrimaryAction={() => handleModalToggle()}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<GroupsCreateModal
|
||||
isCreateModalOpen={isCreateModalOpen}
|
||||
handleModalToggle={handleModalToggle}
|
||||
setIsCreateModalOpen={setIsCreateModalOpen}
|
||||
createGroupName={createGroupName}
|
||||
setCreateGroupName={setCreateGroupName}
|
||||
refresh={refresh}
|
||||
/>
|
||||
{isCreateModalOpen && (
|
||||
<GroupsCreateModal
|
||||
id={id}
|
||||
handleModalToggle={handleModalToggle}
|
||||
refresh={refresh}
|
||||
/>
|
||||
)}
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
168
src/groups/SearchGroups.tsx
Normal file
168
src/groups/SearchGroups.tsx
Normal file
|
@ -0,0 +1,168 @@
|
|||
import React, { useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
Form,
|
||||
InputGroup,
|
||||
PageSection,
|
||||
PageSectionVariants,
|
||||
Text,
|
||||
TextContent,
|
||||
TextInput,
|
||||
} from "@patternfly/react-core";
|
||||
import { SearchIcon } from "@patternfly/react-icons";
|
||||
|
||||
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
|
||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
|
||||
type SearchGroup = GroupRepresentation & {
|
||||
link?: string;
|
||||
};
|
||||
|
||||
export const SearchGroups = () => {
|
||||
const { t } = useTranslation("groups");
|
||||
const adminClient = useAdminClient();
|
||||
const { realm } = useRealm();
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [searchTerms, setSearchTerms] = useState<string[]>([]);
|
||||
const [searchCount, setSearchCount] = useState(0);
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const deleteTerm = (id: string) => {
|
||||
const index = searchTerms.indexOf(id);
|
||||
searchTerms.splice(index, 1);
|
||||
setSearchTerms([...searchTerms]);
|
||||
refresh();
|
||||
};
|
||||
|
||||
const addTerm = () => {
|
||||
setSearchTerms([...searchTerms, searchTerm]);
|
||||
setSearchTerm("");
|
||||
refresh();
|
||||
};
|
||||
|
||||
const GroupNameCell = (group: SearchGroup) => (
|
||||
<>
|
||||
<Link key={group.id} to={`/${realm}/groups/${group.link}`}>
|
||||
{group.name}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
|
||||
const flatten = (
|
||||
groups: GroupRepresentation[],
|
||||
id?: string
|
||||
): SearchGroup[] => {
|
||||
let result: SearchGroup[] = [];
|
||||
for (const group of groups) {
|
||||
const link = `${id || ""}${id ? "/" : ""}${group.id}`;
|
||||
result.push({ ...group, link });
|
||||
if (group.subGroups) {
|
||||
result = [...result, ...flatten(group.subGroups, link)];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const loader = async (first?: number, max?: number) => {
|
||||
const params = {
|
||||
first: first!,
|
||||
max: max!,
|
||||
};
|
||||
|
||||
let result: SearchGroup[] = [];
|
||||
if (searchTerms[0]) {
|
||||
result = await adminClient.groups.find({
|
||||
...params,
|
||||
search: searchTerms[0],
|
||||
});
|
||||
result = flatten(result);
|
||||
for (const searchTerm of searchTerms) {
|
||||
result = result.filter((group) => group.name?.includes(searchTerm));
|
||||
}
|
||||
}
|
||||
|
||||
setSearchCount(result.length);
|
||||
return result;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
<TextContent className="pf-u-mr-sm">
|
||||
<Text component="h1">{t("searchForGroups")}</Text>
|
||||
</TextContent>
|
||||
<Form
|
||||
className="pf-u-mt-sm keycloak__form"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
addTerm();
|
||||
}}
|
||||
>
|
||||
<InputGroup>
|
||||
<TextInput
|
||||
name="search"
|
||||
data-testid="group-search"
|
||||
type="search"
|
||||
aria-label={t("search")}
|
||||
placeholder={t("searchGroups")}
|
||||
value={searchTerm}
|
||||
onChange={(value) => setSearchTerm(value)}
|
||||
/>
|
||||
<Button
|
||||
data-testid="search-button"
|
||||
variant={ButtonVariant.control}
|
||||
aria-label={t("search")}
|
||||
onClick={addTerm}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
<ChipGroup>
|
||||
{searchTerms.map((term) => (
|
||||
<Chip key={term} onClick={() => deleteTerm(term)}>
|
||||
{term}
|
||||
</Chip>
|
||||
))}
|
||||
</ChipGroup>
|
||||
</Form>
|
||||
</PageSection>
|
||||
<PageSection variant={searchCount === 0 ? "light" : "default"}>
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
ariaLabelKey="groups:groups"
|
||||
isPaginated
|
||||
loader={loader}
|
||||
columns={[
|
||||
{
|
||||
name: "name",
|
||||
displayKey: "groups:groupName",
|
||||
cellRenderer: GroupNameCell,
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
displayKey: "groups:path",
|
||||
},
|
||||
]}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
message={t("noSearchResults")}
|
||||
instructions={t("noSearchResultsInstructions")}
|
||||
hasIcon={false}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -5,19 +5,25 @@
|
|||
"groupName": "Group name",
|
||||
"searchForGroups": "Search for groups",
|
||||
"searchGroups": "Search groups",
|
||||
"searchGroup": "Search group",
|
||||
"renameGroup": "Rename group",
|
||||
"deleteGroup": "Delete group",
|
||||
"search": "Search",
|
||||
"members": "Members",
|
||||
"path": "Path",
|
||||
"moveTo": "Move to",
|
||||
"tableOfGroups": "Table of groups",
|
||||
"groupsDescription": "Description goes here",
|
||||
"groupCreated": "Group created",
|
||||
"couldNotCreateGroup": "Could not create group",
|
||||
"couldNotCreateGroup": "Could not create group {{error}}",
|
||||
"createAGroup": "Create a group",
|
||||
"create": "Create",
|
||||
"noSearchResults": "No search results",
|
||||
"noSearchResultsInstructions": "Click on the search bar above to search for groups",
|
||||
"noGroupsInThisRealm": "No groups in this realm",
|
||||
"noGroupsInThisRealmInstructions": "You haven't created any groups in this realm. Create a group to get started.",
|
||||
"noGroupsInThisSubGroup": "No groups in this sub group",
|
||||
"noGroupsInThisSubGroupInstructions": "You haven't created any groups in this sub group.",
|
||||
"groupDelete": "Group deleted",
|
||||
"groupsDeleted": "Groups deleted",
|
||||
"groupDeleteError": "Error deleting group {error}"
|
||||
|
|
|
@ -25,6 +25,7 @@ import { UserFederationKerberosSettings } from "./user-federation/UserFederation
|
|||
import { UserFederationLdapSettings } from "./user-federation/UserFederationLdapSettings";
|
||||
import { RoleMappingForm } from "./client-scopes/add/RoleMappingForm";
|
||||
import { RealmRoleTabs } from "./realm-roles/RealmRoleTabs";
|
||||
import { SearchGroups } from "./groups/SearchGroups";
|
||||
|
||||
export type RouteDef = BreadcrumbsRoute & {
|
||||
access: AccessType;
|
||||
|
@ -244,6 +245,21 @@ export const routes: RoutesFn = (t: TFunction) => [
|
|||
breadcrumb: t("common:home"),
|
||||
access: "anyone",
|
||||
},
|
||||
{
|
||||
path: "/:realm/groups/search",
|
||||
component: SearchGroups,
|
||||
breadcrumb: t("groups:searchGroups"),
|
||||
access: "query-groups",
|
||||
},
|
||||
{
|
||||
path: "/:realm/groups",
|
||||
component: GroupsSection,
|
||||
breadcrumb: null,
|
||||
matchOptions: {
|
||||
exact: false,
|
||||
},
|
||||
access: "query-groups",
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
component: DashboardSection,
|
||||
|
@ -256,13 +272,4 @@ export const routes: RoutesFn = (t: TFunction) => [
|
|||
breadcrumb: null,
|
||||
access: "anyone",
|
||||
},
|
||||
{
|
||||
path: "/:realm/groups",
|
||||
component: GroupsSection,
|
||||
breadcrumb: null,
|
||||
matchOptions: {
|
||||
exact: false,
|
||||
},
|
||||
access: "query-groups",
|
||||
},
|
||||
];
|
||||
|
|
12
types/import.d.ts
vendored
12
types/import.d.ts
vendored
|
@ -6,3 +6,15 @@ interface ImportMeta {
|
|||
hot: any;
|
||||
env: Record<string, any>;
|
||||
}
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable<Subject> {
|
||||
/**
|
||||
* Get one or more DOM elements by `data-testid`.
|
||||
*
|
||||
* @example
|
||||
* cy.getId('searchButton') // Gets the <button data-testid="searchButton">Search</button>
|
||||
*/
|
||||
getId(selector: string, ...args): Chainable<any>;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue