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 ListingPage from "../support/pages/admin_console/ListingPage";
|
||||||
import GroupModal from "../support/pages/admin_console/manage/groups/GroupModal";
|
import GroupModal from "../support/pages/admin_console/manage/groups/GroupModal";
|
||||||
import GroupDetailPage from "../support/pages/admin_console/manage/groups/GroupDetailPage";
|
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 { SearchGroupPage } from "../support/pages/admin_console/manage/groups/SearchGroup";
|
||||||
import Masthead from "../support/pages/admin_console/Masthead";
|
import Masthead from "../support/pages/admin_console/Masthead";
|
||||||
import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
||||||
|
@ -16,6 +17,8 @@ describe("Group test", () => {
|
||||||
const listingPage = new ListingPage();
|
const listingPage = new ListingPage();
|
||||||
const viewHeaderPage = new ViewHeaderPage();
|
const viewHeaderPage = new ViewHeaderPage();
|
||||||
const groupModal = new GroupModal();
|
const groupModal = new GroupModal();
|
||||||
|
const searchGroupPage = new SearchGroupPage();
|
||||||
|
const moveGroupModal = new MoveGroupModal();
|
||||||
|
|
||||||
let groupName = "group";
|
let groupName = "group";
|
||||||
|
|
||||||
|
@ -61,12 +64,36 @@ describe("Group test", () => {
|
||||||
listingPage.deleteItem(newName);
|
listingPage.deleteItem(newName);
|
||||||
});
|
});
|
||||||
|
|
||||||
const searchGroupPage = new SearchGroupPage();
|
|
||||||
it("Group search", () => {
|
it("Group search", () => {
|
||||||
viewHeaderPage.clickAction("searchGroup");
|
viewHeaderPage.clickAction("searchGroup");
|
||||||
searchGroupPage.searchGroup("group").clickSearchButton();
|
searchGroupPage.searchGroup("group").clickSearchButton();
|
||||||
searchGroupPage.checkTerm("group");
|
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", () => {
|
describe("Group details", () => {
|
||||||
|
|
|
@ -47,6 +47,20 @@ export default class ListingPage {
|
||||||
return this;
|
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) {
|
itemExist(itemName: string, exist = true) {
|
||||||
cy.get(this.itemsRows)
|
cy.get(this.itemsRows)
|
||||||
.contains(itemName)
|
.contains(itemName)
|
||||||
|
@ -62,23 +76,15 @@ export default class ListingPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteItem(itemName: string) {
|
deleteItem(itemName: string) {
|
||||||
cy.get(this.itemsRows)
|
this.clickRowDetails(itemName);
|
||||||
.contains(itemName)
|
this.clickDetailMenu("Delete");
|
||||||
.parentsUntil("tbody")
|
|
||||||
.find(this.itemRowDrpDwn)
|
|
||||||
.click();
|
|
||||||
cy.get(this.itemsRows).contains("Delete").click();
|
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportItem(itemName: string) {
|
exportItem(itemName: string) {
|
||||||
cy.get(this.itemsRows)
|
this.clickRowDetails(itemName);
|
||||||
.contains(itemName)
|
this.clickDetailMenu("Export");
|
||||||
.parentsUntil("tbody")
|
|
||||||
.find(this.itemRowDrpDwn)
|
|
||||||
.click();
|
|
||||||
cy.get(this.itemsRows).contains("Export").click();
|
|
||||||
|
|
||||||
return this;
|
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) {
|
} catch (error) {
|
||||||
addAlert(
|
addAlert(
|
||||||
t("deleteError", {
|
t("deleteError", {
|
||||||
error: error.response.data?.errorMessage || error,
|
error: error.response?.data?.errorMessage || error,
|
||||||
}),
|
}),
|
||||||
AlertVariant.danger
|
AlertVariant.danger
|
||||||
);
|
);
|
||||||
|
|
|
@ -21,6 +21,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
|
||||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||||
import { GroupsModal } from "./GroupsModal";
|
import { GroupsModal } from "./GroupsModal";
|
||||||
import { getLastId } from "./groupIdUtils";
|
import { getLastId } from "./groupIdUtils";
|
||||||
|
import { MoveGroupDialog } from "./MoveGroupDialog";
|
||||||
|
|
||||||
type GroupTableData = GroupRepresentation & {
|
type GroupTableData = GroupRepresentation & {
|
||||||
membersLength?: number;
|
membersLength?: number;
|
||||||
|
@ -35,6 +36,7 @@ export const GroupTable = () => {
|
||||||
const [isKebabOpen, setIsKebabOpen] = useState(false);
|
const [isKebabOpen, setIsKebabOpen] = useState(false);
|
||||||
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
|
||||||
const [selectedRows, setSelectedRows] = useState<GroupRepresentation[]>([]);
|
const [selectedRows, setSelectedRows] = useState<GroupRepresentation[]>([]);
|
||||||
|
const [move, setMove] = useState<GroupTableData>();
|
||||||
|
|
||||||
const { subGroups } = useSubGroups();
|
const { subGroups } = useSubGroups();
|
||||||
|
|
||||||
|
@ -161,7 +163,10 @@ export const GroupTable = () => {
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
title: t("moveTo"),
|
title: t("moveTo"),
|
||||||
onRowClick: () => console.log("TO DO: Add move to functionality"),
|
onRowClick: async (group) => {
|
||||||
|
setMove(group);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("common:delete"),
|
title: t("common:delete"),
|
||||||
|
@ -201,6 +206,34 @@ export const GroupTable = () => {
|
||||||
refresh={refresh}
|
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",
|
"includeSubGroups": "Include sub-group users",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
"moveTo": "Move to",
|
"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",
|
"tableOfGroups": "Table of groups",
|
||||||
"groupsDescription": "Description goes here",
|
"groupsDescription": "Description goes here",
|
||||||
"groupCreated": "Group created",
|
"groupCreated": "Group created",
|
||||||
|
|
|
@ -163,7 +163,7 @@ export const RealmRoleTabs = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addAlert(
|
addAlert(
|
||||||
t((id ? "roleSave" : "roleCreate") + "Error", {
|
t((id ? "roleSave" : "roleCreate") + "Error", {
|
||||||
error: error.response.data?.errorMessage || error,
|
error: error.response?.data?.errorMessage || error,
|
||||||
}),
|
}),
|
||||||
AlertVariant.danger
|
AlertVariant.danger
|
||||||
);
|
);
|
||||||
|
|
|
@ -63,7 +63,7 @@ export const NewRealmForm = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addAlert(
|
addAlert(
|
||||||
t("saveRealmError", {
|
t("saveRealmError", {
|
||||||
error: error.response.data?.errorMessage || error,
|
error: error.response?.data?.errorMessage || error,
|
||||||
}),
|
}),
|
||||||
AlertVariant.danger
|
AlertVariant.danger
|
||||||
);
|
);
|
||||||
|
|
|
@ -51,7 +51,7 @@ export const UsersTabs = () => {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addAlert(
|
addAlert(
|
||||||
t("users:userCreateError", {
|
t("users:userCreateError", {
|
||||||
error: error.response.data?.errorMessage || error,
|
error: error.response?.data?.errorMessage || error,
|
||||||
}),
|
}),
|
||||||
AlertVariant.danger
|
AlertVariant.danger
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in a new issue