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:
mfrances17 2020-11-10 15:26:20 -05:00 committed by GitHub
parent c781d86026
commit 42bb5cfe3f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 281 additions and 45 deletions

View file

@ -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

View file

@ -10,6 +10,7 @@ export const ExternalLink = ({ title, href, ...rest }: ButtonProps) => {
iconPosition="right"
component="a"
href={href}
target="_blank"
{...rest}
>
{title ? title : href}

View file

@ -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

View 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>
);
};

View file

@ -0,0 +1,3 @@
.keycloak__keycloak-card__footer-label {
margin-left: var(--pf-global--spacer--lg);
}

View file

@ -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 />
</>

View file

@ -34,6 +34,7 @@ const initOptions = {
...groups,
...users,
...sessions,
...userFederation,
...events,
...storybook,
...userFederation,

View file

@ -1,7 +1,12 @@
import React, { useContext, useEffect, useState } from "react";
import {
AlertVariant,
ButtonVariant,
Card,
CardTitle,
DropdownItem,
Gallery,
GalleryItem,
PageSection,
Split,
SplitItem,
@ -9,60 +14,168 @@ 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>
<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>
</TextContent>
<hr className="pf-u-mb-lg" />
<Gallery hasGutter>
<Card isHoverable>
<CardTitle>
<Split hasGutter>
<SplitItem>
<DatabaseIcon size="lg" />
</SplitItem>
<SplitItem isFilled>{t("addKerberos")}</SplitItem>
</Split>
</CardTitle>
</Card>
<Card isHoverable>
<CardTitle>
<Split hasGutter>
<SplitItem>
<DatabaseIcon size="lg" />
</SplitItem>
<SplitItem isFilled>{t("addLdap")}</SplitItem>
</Split>
</CardTitle>
</Card>
</Gallery>
{userFederations && userFederations.length > 0 ? (
<>
<DeleteConfirm />
<Gallery hasGutter>{cards}</Gallery>
</>
) : (
<>
<TextContent>
<Text component={TextVariants.p}>{t("getStarted")}</Text>
</TextContent>
<TextContent>
<Text className="pf-u-mt-lg" component={TextVariants.h2}>
{t("providers")}
</Text>
</TextContent>
<hr className="pf-u-mb-lg" />
<Gallery hasGutter>
<Card isHoverable>
<CardTitle>
<Split hasGutter>
<SplitItem>
<DatabaseIcon size="lg" />
</SplitItem>
<SplitItem isFilled>{t("addKerberos")}</SplitItem>
</Split>
</CardTitle>
</Card>
<Card isHoverable>
<CardTitle>
<Split hasGutter>
<SplitItem>
<DatabaseIcon size="lg" />
</SplitItem>
<SplitItem isFilled>{t("addLdap")}</SplitItem>
</Split>
</CardTitle>
</Card>
</Gallery>
</>
)}
</PageSection>
</>
);

View file

@ -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."
}
}

View file

@ -0,0 +1,8 @@
export interface UserFederationRepresentation {
id: string;
name: string;
providerId: string;
providerType: string;
parentId: string;
config: { [index: string]: any };
}

View file

@ -0,0 +1,3 @@
.keycloak__user-federation__dropdown {
margin-top: var(--pf-global--spacer--lg);
}