initial move group dialog (#455)
* initial move group dialog * added test * fixed search and added breadcrumb * filter current group * added empty state * add cancel button to modal * fixed merge error
This commit is contained in:
parent
6ea4f88b5b
commit
f10661444d
11 changed files with 369 additions and 18 deletions
|
@ -1,6 +1,7 @@
|
|||
import ListingPage from "../support/pages/admin_console/ListingPage";
|
||||
import GroupModal from "../support/pages/admin_console/manage/groups/GroupModal";
|
||||
import GroupDetailPage from "../support/pages/admin_console/manage/groups/GroupDetailPage";
|
||||
import MoveGroupModal from "../support/pages/admin_console/manage/groups/MoveGroupModal";
|
||||
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";
|
||||
|
@ -16,6 +17,8 @@ describe("Group test", () => {
|
|||
const listingPage = new ListingPage();
|
||||
const viewHeaderPage = new ViewHeaderPage();
|
||||
const groupModal = new GroupModal();
|
||||
const searchGroupPage = new SearchGroupPage();
|
||||
const moveGroupModal = new MoveGroupModal();
|
||||
|
||||
let groupName = "group";
|
||||
|
||||
|
@ -61,12 +64,36 @@ describe("Group test", () => {
|
|||
listingPage.deleteItem(newName);
|
||||
});
|
||||
|
||||
const searchGroupPage = new SearchGroupPage();
|
||||
it("Group search", () => {
|
||||
viewHeaderPage.clickAction("searchGroup");
|
||||
searchGroupPage.searchGroup("group").clickSearchButton();
|
||||
searchGroupPage.checkTerm("group");
|
||||
});
|
||||
|
||||
it("Should move group", () => {
|
||||
const targetGroupName = "target";
|
||||
groupModal
|
||||
.open("empty-primary-action")
|
||||
.fillGroupForm(groupName)
|
||||
.clickCreate();
|
||||
|
||||
groupModal.open().fillGroupForm(targetGroupName).clickCreate();
|
||||
|
||||
listingPage.clickRowDetails(groupName).clickDetailMenu("Move to");
|
||||
moveGroupModal
|
||||
.clickRow(targetGroupName)
|
||||
.checkTitle(`Move ${groupName} to ${targetGroupName}`);
|
||||
moveGroupModal.clickMove();
|
||||
|
||||
masthead.checkNotificationMessage("Group moved");
|
||||
listingPage
|
||||
.itemExist(groupName, false)
|
||||
.goToItemDetails(targetGroupName)
|
||||
.itemExist(targetGroupName);
|
||||
|
||||
sidebarPage.goToGroups();
|
||||
listingPage.deleteItem(targetGroupName);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Group details", () => {
|
||||
|
|
|
@ -47,6 +47,20 @@ export default class ListingPage {
|
|||
return this;
|
||||
}
|
||||
|
||||
clickRowDetails(itemName: string) {
|
||||
cy.get(this.itemsRows)
|
||||
.contains(itemName)
|
||||
.parentsUntil("tbody")
|
||||
.find(this.itemRowDrpDwn)
|
||||
.click();
|
||||
return this;
|
||||
}
|
||||
|
||||
clickDetailMenu(name: string) {
|
||||
cy.get(this.itemsRows).contains(name).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
itemExist(itemName: string, exist = true) {
|
||||
cy.get(this.itemsRows)
|
||||
.contains(itemName)
|
||||
|
@ -62,23 +76,15 @@ export default class ListingPage {
|
|||
}
|
||||
|
||||
deleteItem(itemName: string) {
|
||||
cy.get(this.itemsRows)
|
||||
.contains(itemName)
|
||||
.parentsUntil("tbody")
|
||||
.find(this.itemRowDrpDwn)
|
||||
.click();
|
||||
cy.get(this.itemsRows).contains("Delete").click();
|
||||
this.clickRowDetails(itemName);
|
||||
this.clickDetailMenu("Delete");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
exportItem(itemName: string) {
|
||||
cy.get(this.itemsRows)
|
||||
.contains(itemName)
|
||||
.parentsUntil("tbody")
|
||||
.find(this.itemRowDrpDwn)
|
||||
.click();
|
||||
cy.get(this.itemsRows).contains("Export").click();
|
||||
this.clickRowDetails(itemName);
|
||||
this.clickDetailMenu("Export");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
export default class MoveGroupModal {
|
||||
private moveButton = "moveGroup";
|
||||
private title = ".pf-c-modal-box__title";
|
||||
|
||||
clickRow(groupName: string) {
|
||||
cy.getId(groupName).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
checkTitle(title: string) {
|
||||
cy.get(this.title).should("have.text", title);
|
||||
return this;
|
||||
}
|
||||
|
||||
clickMove() {
|
||||
cy.getId(this.moveButton).click();
|
||||
return this;
|
||||
}
|
||||
}
|
50
nginx.conf
Normal file
50
nginx.conf
Normal file
|
@ -0,0 +1,50 @@
|
|||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name localhost;
|
||||
|
||||
#charset koi8-r;
|
||||
#access_log /var/log/nginx/host.access.log main;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location /auth/ {
|
||||
proxy_pass http://localhost:8180/auth/;
|
||||
}
|
||||
|
||||
|
||||
#error_page 404 /404.html;
|
||||
|
||||
# redirect server error pages to the static page /50x.html
|
||||
#
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
|
||||
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
|
||||
#
|
||||
#location ~ \.php$ {
|
||||
# proxy_pass http://127.0.0.1;
|
||||
#}
|
||||
|
||||
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
|
||||
#
|
||||
#location ~ \.php$ {
|
||||
# root html;
|
||||
# fastcgi_pass 127.0.0.1:9000;
|
||||
# fastcgi_index index.php;
|
||||
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
|
||||
# include fastcgi_params;
|
||||
#}
|
||||
|
||||
# deny access to .htaccess files, if Apache's document root
|
||||
# concurs with nginx's one
|
||||
#
|
||||
#location ~ /\.ht {
|
||||
# deny all;
|
||||
#}
|
||||
}
|
|
@ -57,7 +57,7 @@ export const ClientScopesSection = () => {
|
|||
} catch (error) {
|
||||
addAlert(
|
||||
t("deleteError", {
|
||||
error: error.response.data?.errorMessage || error,
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
|
|
|
@ -21,6 +21,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
|
|||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
import { GroupsModal } from "./GroupsModal";
|
||||
import { getLastId } from "./groupIdUtils";
|
||||
import { MoveGroupDialog } from "./MoveGroupDialog";
|
||||
|
||||
type GroupTableData = GroupRepresentation & {
|
||||
membersLength?: number;
|
||||
|
@ -35,6 +36,7 @@ export const GroupTable = () => {
|
|||
const [isKebabOpen, setIsKebabOpen] = useState(false);
|
||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<GroupRepresentation[]>([]);
|
||||
const [move, setMove] = useState<GroupTableData>();
|
||||
|
||||
const { subGroups } = useSubGroups();
|
||||
|
||||
|
@ -161,7 +163,10 @@ export const GroupTable = () => {
|
|||
actions={[
|
||||
{
|
||||
title: t("moveTo"),
|
||||
onRowClick: () => console.log("TO DO: Add move to functionality"),
|
||||
onRowClick: async (group) => {
|
||||
setMove(group);
|
||||
return false;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t("common:delete"),
|
||||
|
@ -201,6 +206,34 @@ export const GroupTable = () => {
|
|||
refresh={refresh}
|
||||
/>
|
||||
)}
|
||||
{move && (
|
||||
<MoveGroupDialog
|
||||
group={move}
|
||||
onClose={() => setMove(undefined)}
|
||||
onMove={async (id) => {
|
||||
delete move.membersLength;
|
||||
try {
|
||||
try {
|
||||
await adminClient.groups.setOrCreateChild({ id }, move);
|
||||
} catch (error) {
|
||||
if (error.response) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
setMove(undefined);
|
||||
refresh();
|
||||
addAlert(t("moveGroupSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("moveGroupError", {
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
210
src/groups/MoveGroupDialog.tsx
Normal file
210
src/groups/MoveGroupDialog.tsx
Normal file
|
@ -0,0 +1,210 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
DataList,
|
||||
DataListAction,
|
||||
DataListCell,
|
||||
DataListItem,
|
||||
DataListItemCells,
|
||||
DataListItemRow,
|
||||
InputGroup,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
TextInput,
|
||||
Toolbar,
|
||||
ToolbarContent,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
import { AngleRightIcon, SearchIcon } from "@patternfly/react-icons";
|
||||
|
||||
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
|
||||
import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient";
|
||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
|
||||
type MoveGroupDialogProps = {
|
||||
group: GroupRepresentation;
|
||||
onClose: () => void;
|
||||
onMove: (groupId: string) => void;
|
||||
};
|
||||
|
||||
export const MoveGroupDialog = ({
|
||||
group,
|
||||
onClose,
|
||||
onMove,
|
||||
}: MoveGroupDialogProps) => {
|
||||
const { t } = useTranslation("groups");
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
const errorHandler = useErrorHandler();
|
||||
|
||||
const [navigation, setNavigation] = useState<GroupRepresentation[]>([]);
|
||||
const [groups, setGroups] = useState<GroupRepresentation[]>([]);
|
||||
const [filtered, setFiltered] = useState<GroupRepresentation[]>();
|
||||
const [filter, setFilter] = useState("");
|
||||
|
||||
const [id, setId] = useState<string>();
|
||||
const currentGroup = () => navigation[navigation.length - 1];
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
asyncStateFetch(
|
||||
async () => {
|
||||
if (id) {
|
||||
const group = await adminClient.groups.findOne({ id });
|
||||
return { group, groups: group.subGroups! };
|
||||
} else {
|
||||
return { groups: await adminClient.groups.find() };
|
||||
}
|
||||
},
|
||||
({ group: selectedGroup, groups }) => {
|
||||
if (selectedGroup) setNavigation([...navigation, selectedGroup]);
|
||||
setGroups(groups.filter((g) => g.id !== group.id));
|
||||
},
|
||||
errorHandler
|
||||
),
|
||||
[id]
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.large}
|
||||
title={
|
||||
currentGroup()
|
||||
? t("moveToGroup", {
|
||||
group1: group.name,
|
||||
group2: currentGroup().name,
|
||||
})
|
||||
: t("moveTo")
|
||||
}
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="moveGroup"
|
||||
key="confirm"
|
||||
variant="primary"
|
||||
form="group-form"
|
||||
onClick={() => onMove(currentGroup().id!)}
|
||||
>
|
||||
{t("moveHere")}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="moveCancel"
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Breadcrumb>
|
||||
<BreadcrumbItem key="home">
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => {
|
||||
setId(undefined);
|
||||
setNavigation([]);
|
||||
}}
|
||||
>
|
||||
{t("groups")}
|
||||
</Button>
|
||||
</BreadcrumbItem>
|
||||
{navigation.map((group, i) => (
|
||||
<BreadcrumbItem key={i}>
|
||||
{navigation.length - 1 !== i && (
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => {
|
||||
setId(group.id);
|
||||
setNavigation([...navigation].slice(0, i));
|
||||
}}
|
||||
>
|
||||
{group.name}
|
||||
</Button>
|
||||
)}
|
||||
{navigation.length - 1 === i && <>{group.name}</>}
|
||||
</BreadcrumbItem>
|
||||
))}
|
||||
</Breadcrumb>
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarContent>
|
||||
<ToolbarItem>
|
||||
<InputGroup>
|
||||
<TextInput
|
||||
type="search"
|
||||
aria-label={t("common:search")}
|
||||
placeholder={t("searchForGroups")}
|
||||
onChange={(value) => {
|
||||
if (value === "") {
|
||||
setFiltered(undefined);
|
||||
}
|
||||
setFilter(value);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant={ButtonVariant.control}
|
||||
aria-label={t("common:search")}
|
||||
onClick={() =>
|
||||
setFiltered(
|
||||
groups.filter((group) =>
|
||||
group.name?.toLowerCase().includes(filter.toLowerCase())
|
||||
)
|
||||
)
|
||||
}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</ToolbarItem>
|
||||
</ToolbarContent>
|
||||
</Toolbar>
|
||||
<DataList
|
||||
onSelectDataListItem={(value) => setId(value)}
|
||||
aria-label={t("groups")}
|
||||
isCompact
|
||||
>
|
||||
{(filtered || groups).map((group) => (
|
||||
<DataListItem
|
||||
aria-labelledby={group.name}
|
||||
key={group.id}
|
||||
id={group.id}
|
||||
>
|
||||
<DataListItemRow data-testid={group.name}>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell key={`name-${group.id}`}>
|
||||
<>{group.name}</>
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
<DataListAction
|
||||
aria-labelledby={`select-${group.name}`}
|
||||
id={`select-${group.name}`}
|
||||
aria-label={t("groupName")}
|
||||
isPlainButtonAction
|
||||
>
|
||||
<Button isDisabled variant="link">
|
||||
<AngleRightIcon />
|
||||
</Button>
|
||||
</DataListAction>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
))}
|
||||
{(filtered || groups).length === 0 && (
|
||||
<ListEmptyState
|
||||
hasIcon={false}
|
||||
message={t("moveGroupEmpty")}
|
||||
instructions={t("moveGroupEmptyInstructions")}
|
||||
/>
|
||||
)}
|
||||
</DataList>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -23,6 +23,12 @@
|
|||
"includeSubGroups": "Include sub-group users",
|
||||
"path": "Path",
|
||||
"moveTo": "Move to",
|
||||
"moveToGroup": "Move {{group1}} to {{group2}}",
|
||||
"moveHere": "Move here",
|
||||
"moveGroupEmpty": "No sub groups",
|
||||
"moveGroupEmptyInstructions": "There are no sub groups, select 'Move here' to move the selected group as a subgroup of this group",
|
||||
"moveGroupSuccess": "Group moved",
|
||||
"moveGroupError": "Could not move group {{error}}",
|
||||
"tableOfGroups": "Table of groups",
|
||||
"groupsDescription": "Description goes here",
|
||||
"groupCreated": "Group created",
|
||||
|
|
|
@ -163,7 +163,7 @@ export const RealmRoleTabs = () => {
|
|||
} catch (error) {
|
||||
addAlert(
|
||||
t((id ? "roleSave" : "roleCreate") + "Error", {
|
||||
error: error.response.data?.errorMessage || error,
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
|
|
|
@ -63,7 +63,7 @@ export const NewRealmForm = () => {
|
|||
} catch (error) {
|
||||
addAlert(
|
||||
t("saveRealmError", {
|
||||
error: error.response.data?.errorMessage || error,
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
|
|
|
@ -51,7 +51,7 @@ export const UsersTabs = () => {
|
|||
} catch (error) {
|
||||
addAlert(
|
||||
t("users:userCreateError", {
|
||||
error: error.response.data?.errorMessage || error,
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue