User Federation card view implementation (#208)
* initial html css changes * additional look and feel * merge empty state changes * fix dup key * pull in real data * real data and empty * fix bad paren * add delete functionality * rm debug msg * fix rediculous prettier trailing commas * changes from PR * css changes from PR * use good userfed info link * use consistent state names * fix msg merge issue * update and use ViewHeader * fix broken tests * add and use generic card component * additional empty state behavior * delete confirm modal
This commit is contained in:
parent
c781d86026
commit
42bb5cfe3f
11 changed files with 281 additions and 45 deletions
|
@ -102,6 +102,7 @@ Object {
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="http://blog.nerdin.chrealms/master/account/"
|
||||
target="_blank"
|
||||
>
|
||||
http://blog.nerdin.chrealms/master/account/
|
||||
<span
|
||||
|
@ -201,6 +202,7 @@ Object {
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="http://blog.nerdin.chrealms/master/account/"
|
||||
target="_blank"
|
||||
>
|
||||
http://blog.nerdin.chrealms/master/account/
|
||||
<span
|
||||
|
@ -517,6 +519,7 @@ Object {
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="http://localhost:8080/"
|
||||
target="_blank"
|
||||
>
|
||||
http://localhost:8080/
|
||||
<span
|
||||
|
@ -687,6 +690,7 @@ Object {
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="http://blog.nerdin.chadmin/master/console/"
|
||||
target="_blank"
|
||||
>
|
||||
http://blog.nerdin.chadmin/master/console/
|
||||
<span
|
||||
|
@ -845,6 +849,7 @@ Object {
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="http://blog.nerdin.chrealms/master/account/"
|
||||
target="_blank"
|
||||
>
|
||||
http://blog.nerdin.chrealms/master/account/
|
||||
<span
|
||||
|
@ -944,6 +949,7 @@ Object {
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="http://blog.nerdin.chrealms/master/account/"
|
||||
target="_blank"
|
||||
>
|
||||
http://blog.nerdin.chrealms/master/account/
|
||||
<span
|
||||
|
@ -1260,6 +1266,7 @@ Object {
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="http://localhost:8080/"
|
||||
target="_blank"
|
||||
>
|
||||
http://localhost:8080/
|
||||
<span
|
||||
|
@ -1430,6 +1437,7 @@ Object {
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="http://blog.nerdin.chadmin/master/console/"
|
||||
target="_blank"
|
||||
>
|
||||
http://blog.nerdin.chadmin/master/console/
|
||||
<span
|
||||
|
|
|
@ -10,6 +10,7 @@ export const ExternalLink = ({ title, href, ...rest }: ButtonProps) => {
|
|||
iconPosition="right"
|
||||
component="a"
|
||||
href={href}
|
||||
target="_blank"
|
||||
{...rest}
|
||||
>
|
||||
{title ? title : href}
|
||||
|
|
|
@ -9,6 +9,7 @@ exports[`<ExternalLink /> render as application 1`] = `
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="/application/main"
|
||||
target="_blank"
|
||||
>
|
||||
Application link
|
||||
</a>
|
||||
|
@ -24,6 +25,7 @@ exports[`<ExternalLink /> render with internal url 1`] = `
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="/application/home/"
|
||||
target="_blank"
|
||||
>
|
||||
Application page
|
||||
</a>
|
||||
|
@ -39,6 +41,7 @@ exports[`<ExternalLink /> render with link 1`] = `
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="http://hello.nl/"
|
||||
target="_blank"
|
||||
>
|
||||
http://hello.nl/
|
||||
<span
|
||||
|
@ -71,6 +74,7 @@ exports[`<ExternalLink /> render with link and title 1`] = `
|
|||
data-ouia-component-type="PF4/Button"
|
||||
data-ouia-safe="true"
|
||||
href="http://hello.nl/"
|
||||
target="_blank"
|
||||
>
|
||||
Link to Hello
|
||||
<span
|
||||
|
|
64
src/components/keycloak-card/KeycloakCard.tsx
Normal file
64
src/components/keycloak-card/KeycloakCard.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import React, { ReactElement, useState } from "react";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardActions,
|
||||
CardTitle,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
Dropdown,
|
||||
KebabToggle,
|
||||
Label,
|
||||
} from "@patternfly/react-core";
|
||||
// import { useTranslation } from "react-i18next";
|
||||
import "./keycloak-card.css";
|
||||
|
||||
export type KeycloakCardProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
dropdownItems?: ReactElement[];
|
||||
labelText?: string;
|
||||
footerText?: string;
|
||||
configEnabled?: boolean;
|
||||
providerId?: string;
|
||||
};
|
||||
|
||||
export const KeycloakCard = ({
|
||||
dropdownItems,
|
||||
title,
|
||||
labelText,
|
||||
footerText,
|
||||
}: KeycloakCardProps) => {
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const onDropdownToggle = () => {
|
||||
setIsDropdownOpen(!isDropdownOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardActions>
|
||||
{dropdownItems && (
|
||||
<Dropdown
|
||||
isPlain
|
||||
position={"right"}
|
||||
toggle={<KebabToggle onToggle={onDropdownToggle} />}
|
||||
isOpen={isDropdownOpen}
|
||||
dropdownItems={dropdownItems}
|
||||
/>
|
||||
)}
|
||||
</CardActions>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody />
|
||||
<CardFooter>
|
||||
{footerText && footerText}
|
||||
{labelText && (
|
||||
<Label color="blue" className="keycloak__keycloak-card__footer-label">
|
||||
{labelText}
|
||||
</Label>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
3
src/components/keycloak-card/keycloak-card.css
Normal file
3
src/components/keycloak-card/keycloak-card.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.keycloak__keycloak-card__footer-label {
|
||||
margin-left: var(--pf-global--spacer--lg);
|
||||
}
|
|
@ -19,7 +19,6 @@ import {
|
|||
import { HelpContext } from "../help-enabler/HelpHeader";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ExternalLink } from "../external-link/ExternalLink";
|
||||
import { isRowExpanded } from "@patternfly/react-table";
|
||||
|
||||
export type ViewHeaderProps = {
|
||||
titleKey: string;
|
||||
|
@ -27,6 +26,8 @@ export type ViewHeaderProps = {
|
|||
subKey: string;
|
||||
subKeyLinkProps?: ButtonProps;
|
||||
dropdownItems?: ReactElement[];
|
||||
lowerDropdownItems?: any;
|
||||
lowerDropdownMenuTitle?: any;
|
||||
isEnabled?: boolean;
|
||||
onToggle?: (value: boolean) => void;
|
||||
};
|
||||
|
@ -37,17 +38,24 @@ export const ViewHeader = ({
|
|||
subKey,
|
||||
subKeyLinkProps,
|
||||
dropdownItems,
|
||||
lowerDropdownMenuTitle,
|
||||
lowerDropdownItems,
|
||||
isEnabled = true,
|
||||
onToggle,
|
||||
}: ViewHeaderProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { enabled } = useContext(HelpContext);
|
||||
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
||||
const [isLowerDropdownOpen, setIsLowerDropdownOpen] = useState(false);
|
||||
|
||||
const onDropdownToggle = () => {
|
||||
setDropdownOpen(!isDropdownOpen);
|
||||
};
|
||||
|
||||
const onLowerDropdownToggle = () => {
|
||||
setIsLowerDropdownOpen(!isLowerDropdownOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant="light">
|
||||
|
@ -118,6 +126,22 @@ export const ViewHeader = ({
|
|||
</Text>
|
||||
</TextContent>
|
||||
)}
|
||||
{lowerDropdownItems && (
|
||||
<Dropdown
|
||||
className="keycloak__user-federation__dropdown"
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
onToggle={() => onLowerDropdownToggle()}
|
||||
isPrimary
|
||||
id="ufToggleId"
|
||||
>
|
||||
{t(lowerDropdownMenuTitle)}
|
||||
</DropdownToggle>
|
||||
}
|
||||
isOpen={isLowerDropdownOpen}
|
||||
dropdownItems={lowerDropdownItems}
|
||||
/>
|
||||
)}
|
||||
</PageSection>
|
||||
<Divider />
|
||||
</>
|
||||
|
|
|
@ -34,6 +34,7 @@ const initOptions = {
|
|||
...groups,
|
||||
...users,
|
||||
...sessions,
|
||||
...userFederation,
|
||||
...events,
|
||||
...storybook,
|
||||
...userFederation,
|
||||
|
|
|
@ -1,7 +1,12 @@
|
|||
import React, { useContext, useEffect, useState } from "react";
|
||||
import {
|
||||
AlertVariant,
|
||||
ButtonVariant,
|
||||
Card,
|
||||
CardTitle,
|
||||
DropdownItem,
|
||||
Gallery,
|
||||
GalleryItem,
|
||||
PageSection,
|
||||
Split,
|
||||
SplitItem,
|
||||
|
@ -9,36 +14,142 @@ import {
|
|||
TextContent,
|
||||
TextVariants,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { KeycloakCard } from "../components/keycloak-card/KeycloakCard";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { DatabaseIcon } from "@patternfly/react-icons";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import React from "react";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { RealmContext } from "../context/realm-context/RealmContext";
|
||||
import { HttpClientContext } from "../context/http-service/HttpClientContext";
|
||||
import { UserFederationRepresentation } from "./model/userFederation";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import "./user-federation.css";
|
||||
|
||||
export const UserFederationSection = () => {
|
||||
const { t } = useTranslation("user-federation");
|
||||
const linkArgs = {
|
||||
title: t("common:learnMore"),
|
||||
href: "http://google.com",
|
||||
const [userFederations, setUserFederations] = useState<
|
||||
UserFederationRepresentation[]
|
||||
>();
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const loader = async () => {
|
||||
const testParams: { [name: string]: string | number } = {
|
||||
parentId: realm,
|
||||
type: "org.keycloak.storage.UserStorageProvider", // MF note that this is providerType in the output, but API call is still type
|
||||
};
|
||||
const result = await httpClient.doGet<UserFederationRepresentation[]>(
|
||||
`/admin/realms/${realm}/components`,
|
||||
{
|
||||
params: testParams,
|
||||
}
|
||||
);
|
||||
setUserFederations(result.data);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loader();
|
||||
}, []);
|
||||
|
||||
const { t } = useTranslation("user-federation");
|
||||
const httpClient = useContext(HttpClientContext)!;
|
||||
const { realm } = useContext(RealmContext);
|
||||
|
||||
const ufAddProviderDropdownItems = [
|
||||
<DropdownItem key="itemLDAP">LDAP</DropdownItem>,
|
||||
<DropdownItem key="itemKerberos">Kerberos</DropdownItem>,
|
||||
];
|
||||
|
||||
const learnMoreLinkProps = {
|
||||
title: `${t("common:learnMore")}`,
|
||||
href:
|
||||
"https://www.keycloak.org/docs/latest/server_admin/index.html#_user-storage-federation",
|
||||
};
|
||||
|
||||
let cards;
|
||||
|
||||
const [currentCard, setCurrentCard] = useState("");
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: t("userFedDeleteConfirmTitle"),
|
||||
messageKey: t("userFedDeleteConfirm"),
|
||||
continueButtonLabel: "common:delete",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
httpClient
|
||||
.doDelete(`/admin/realms/${realm}/components/${currentCard}`)
|
||||
.then(() => loader());
|
||||
addAlert(t("userFedDeletedSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(t("userFedDeleteError", { error }), AlertVariant.danger);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const toggleDeleteForCard = (id: string) => {
|
||||
setCurrentCard(id);
|
||||
toggleDeleteDialog();
|
||||
};
|
||||
|
||||
if (userFederations) {
|
||||
cards = userFederations.map((userFederation, index) => {
|
||||
const ufCardDropdownItems = [
|
||||
<DropdownItem
|
||||
key={`${index}-cardDelete`}
|
||||
onClick={() => {
|
||||
toggleDeleteForCard(userFederation.id);
|
||||
}}
|
||||
>
|
||||
{t("common:delete")}
|
||||
</DropdownItem>,
|
||||
];
|
||||
return (
|
||||
<GalleryItem key={index}>
|
||||
<KeycloakCard
|
||||
id={userFederation.id}
|
||||
dropdownItems={ufCardDropdownItems}
|
||||
title={userFederation.name}
|
||||
footerText={
|
||||
userFederation.providerId === "ldap" ? "LDAP" : "Kerberos"
|
||||
}
|
||||
labelText={
|
||||
userFederation.config.enabled
|
||||
? `${t("common:enabled")}`
|
||||
: `${t("common:disabled")}`
|
||||
}
|
||||
/>
|
||||
</GalleryItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader
|
||||
titleKey="user-federation:userFederation"
|
||||
subKey={"user-federation:descriptionLanding"}
|
||||
subKeyLinkProps={linkArgs}
|
||||
titleKey="userFederation"
|
||||
subKey="user-federation:userFederationExplanation"
|
||||
subKeyLinkProps={learnMoreLinkProps}
|
||||
{...(userFederations && userFederations.length > 0
|
||||
? {
|
||||
lowerDropdownItems: { ufAddProviderDropdownItems },
|
||||
lowerDropdownMenuTitle: "user-federation:addNewProvider",
|
||||
}
|
||||
: {})}
|
||||
/>
|
||||
<PageSection>
|
||||
{userFederations && userFederations.length > 0 ? (
|
||||
<>
|
||||
<DeleteConfirm />
|
||||
<Gallery hasGutter>{cards}</Gallery>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.p}>
|
||||
{t("userFederationExplanation")}
|
||||
</Text>
|
||||
<Text component={TextVariants.p}>{t("getStarted")}</Text>
|
||||
</TextContent>
|
||||
</PageSection>
|
||||
<PageSection isFilled>
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h2}>{t("providers")}</Text>
|
||||
<Text className="pf-u-mt-lg" component={TextVariants.h2}>
|
||||
{t("providers")}
|
||||
</Text>
|
||||
</TextContent>
|
||||
<hr className="pf-u-mb-lg" />
|
||||
<Gallery hasGutter>
|
||||
|
@ -63,6 +174,8 @@ export const UserFederationSection = () => {
|
|||
</CardTitle>
|
||||
</Card>
|
||||
</Gallery>
|
||||
</>
|
||||
)}
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -64,6 +64,13 @@
|
|||
"keyTab": "Key tab",
|
||||
"debug": "Debug",
|
||||
"allowPasswordAuthentication": "Allow password authentication",
|
||||
"updateFirstLogin": "Update first login"
|
||||
"updateFirstLogin": "Update first login",
|
||||
|
||||
"learnMore": "Learn more",
|
||||
"addNewProvider": "Add new provider",
|
||||
"userFedDeletedSuccess": "The user federation provider has been deleted.",
|
||||
"userFedDeleteError": "Could not delete user federation provider: '{{error}}'",
|
||||
"userFedDeleteConfirmTitle": "Delete user federation provider?",
|
||||
"userFedDeleteConfirm": "If you delete this user federation provider, all associated data will be removed."
|
||||
}
|
||||
}
|
||||
|
|
8
src/user-federation/model/userFederation.ts
Normal file
8
src/user-federation/model/userFederation.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export interface UserFederationRepresentation {
|
||||
id: string;
|
||||
name: string;
|
||||
providerId: string;
|
||||
providerType: string;
|
||||
parentId: string;
|
||||
config: { [index: string]: any };
|
||||
}
|
3
src/user-federation/user-federation.css
Normal file
3
src/user-federation/user-federation.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.keycloak__user-federation__dropdown {
|
||||
margin-top: var(--pf-global--spacer--lg);
|
||||
}
|
Loading…
Reference in a new issue