Users(Consents): add empty state and list consents (#516)

* user consents

* remove form prop

* address PR feedback from Stan and list consents in data table

* update test

* revert css updates
This commit is contained in:
Eugenia 2021-04-14 14:39:21 -04:00 committed by GitHub
parent 6f4ea86ecb
commit 2bcdf51075
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 134 additions and 6 deletions

View file

@ -126,8 +126,19 @@ describe("Users test", () => {
cy.getId("modalConfirm").click(); cy.getId("modalConfirm").click();
}); });
it("Delete user", function () { it("Go to user consents test", function () {
cy.wait(1000);
listingPage.searchItem(itemId).itemExist(itemId); listingPage.searchItem(itemId).itemExist(itemId);
cy.wait(1000);
listingPage.goToItemDetails(itemId);
cy.getId("user-consents-tab").click();
cy.getId("empty-state").contains("No consents");
});
it("Delete user test", function () {
// Delete // Delete
cy.wait(1000); cy.wait(1000);
listingPage.deleteItem(itemId); listingPage.deleteItem(itemId);

View file

@ -2,6 +2,7 @@
"client-scopes": { "client-scopes": {
"createClientScope": "Create client scope", "createClientScope": "Create client scope",
"clientScopeList": "Client scopes", "clientScopeList": "Client scopes",
"grantedClientScopes": "Granted client scopes",
"clientScopeDetails": "Client scope details", "clientScopeDetails": "Client scope details",
"clientScopeExplain": "Client scopes allow you to define a common set of protocol mappers and roles, which are shared between multiple clients", "clientScopeExplain": "Client scopes allow you to define a common set of protocol mappers and roles, which are shared between multiple clients",
"searchFor": "Search for client scope", "searchFor": "Search for client scope",

View file

@ -99,6 +99,7 @@ export const ClientsSection = () => {
<DeleteConfirm /> <DeleteConfirm />
<KeycloakDataTable <KeycloakDataTable
key={key} key={key}
emptyState={<> </>}
loader={loader} loader={loader}
isPaginated isPaginated
ariaLabelKey="clients:clientList" ariaLabelKey="clients:clientList"

View file

@ -83,6 +83,8 @@
"tokenDeleteSuccess": "initial access token created successfully", "tokenDeleteSuccess": "initial access token created successfully",
"tokenDeleteError": "Could not delete initial access token: '{{error}}'", "tokenDeleteError": "Could not delete initial access token: '{{error}}'",
"timestamp": "Created date", "timestamp": "Created date",
"created": "Created",
"lastUpdated": "Last updated",
"expires": "Expires", "expires": "Expires",
"count": "Count", "count": "Count",
"remainingCount": "Remaining count", "remainingCount": "Remaining count",

View file

@ -8,6 +8,7 @@ import {
ButtonVariant, ButtonVariant,
EmptyStateSecondaryActions, EmptyStateSecondaryActions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon";
import { PlusCircleIcon } from "@patternfly/react-icons"; import { PlusCircleIcon } from "@patternfly/react-icons";
import { SearchIcon } from "@patternfly/react-icons"; import { SearchIcon } from "@patternfly/react-icons";
@ -23,6 +24,7 @@ export type ListEmptyStateProps = {
primaryActionText?: string; primaryActionText?: string;
onPrimaryAction?: MouseEventHandler<HTMLButtonElement>; onPrimaryAction?: MouseEventHandler<HTMLButtonElement>;
hasIcon?: boolean; hasIcon?: boolean;
icon?: React.ComponentClass<SVGIconProps>;
isSearchVariant?: boolean; isSearchVariant?: boolean;
secondaryActions?: Action[]; secondaryActions?: Action[];
}; };
@ -35,6 +37,7 @@ export const ListEmptyState = ({
isSearchVariant, isSearchVariant,
primaryActionText, primaryActionText,
secondaryActions, secondaryActions,
icon,
}: ListEmptyStateProps) => { }: ListEmptyStateProps) => {
return ( return (
<> <>
@ -42,7 +45,7 @@ export const ListEmptyState = ({
{hasIcon && isSearchVariant ? ( {hasIcon && isSearchVariant ? (
<EmptyStateIcon icon={SearchIcon} /> <EmptyStateIcon icon={SearchIcon} />
) : ( ) : (
hasIcon && <EmptyStateIcon icon={PlusCircleIcon} /> hasIcon && <EmptyStateIcon icon={icon ? icon : PlusCircleIcon} />
)} )}
<Title headingLevel="h4" size="lg"> <Title headingLevel="h4" size="lg">
{message} {message}

View file

@ -18,6 +18,7 @@ import _ from "lodash";
import { PaginatingTableToolbar } from "./PaginatingTableToolbar"; import { PaginatingTableToolbar } from "./PaginatingTableToolbar";
import { asyncStateFetch } from "../../context/auth/AdminClient"; import { asyncStateFetch } from "../../context/auth/AdminClient";
import { ListEmptyState } from "../list-empty-state/ListEmptyState"; import { ListEmptyState } from "../list-empty-state/ListEmptyState";
import { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon";
type Row<T> = { type Row<T> = {
data: T; data: T;
@ -98,6 +99,7 @@ export type DataListProps<T> = {
searchTypeComponent?: ReactNode; searchTypeComponent?: ReactNode;
toolbarItem?: ReactNode; toolbarItem?: ReactNode;
emptyState?: ReactNode; emptyState?: ReactNode;
icon?: React.ComponentClass<SVGIconProps>;
}; };
/** /**
@ -136,6 +138,7 @@ export function KeycloakDataTable<T>({
searchTypeComponent, searchTypeComponent,
toolbarItem, toolbarItem,
emptyState, emptyState,
icon,
...props ...props
}: DataListProps<T>) { }: DataListProps<T>) {
const { t } = useTranslation(); const { t } = useTranslation();
@ -330,6 +333,7 @@ export function KeycloakDataTable<T>({
searchPlaceholderKey && ( searchPlaceholderKey && (
<ListEmptyState <ListEmptyState
hasIcon={true} hasIcon={true}
icon={icon}
isSearchVariant={true} isSearchVariant={true}
message={t("noSearchResults")} message={t("noSearchResults")}
instructions={t("noSearchResultsInstructions")} instructions={t("noSearchResultsInstructions")}

94
src/user/UserConsents.tsx Normal file
View file

@ -0,0 +1,94 @@
import React from "react";
import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { PageSection } from "@patternfly/react-core";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { emptyFormatter } from "../util";
import { useAdminClient } from "../context/auth/AdminClient";
import { cellWidth } from "@patternfly/react-table";
import _ from "lodash";
import UserConsentRepresentation from "keycloak-admin/lib/defs/userConsentRepresentation";
import { CubesIcon } from "@patternfly/react-icons";
import moment from "moment";
export const UserConsents = () => {
const { t } = useTranslation("roles");
const adminClient = useAdminClient();
const { id } = useParams<{ id: string }>();
const alphabetize = (consentsList: UserConsentRepresentation[]) => {
return _.sortBy(consentsList, (client) => client.clientId?.toUpperCase());
};
const loader = async () => {
const consents = await adminClient.users.listConsents({ id });
return alphabetize(consents);
};
const clientScopesRenderer = ({
grantedClientScopes,
}: UserConsentRepresentation) => {
return <>{grantedClientScopes!.join(", ")}</>;
};
const createdRenderer = ({ createDate }: UserConsentRepresentation) => {
return <>{moment(createDate).format("MM/DD/YY hh:MM A")}</>;
};
const lastUpdatedRenderer = ({
lastUpdatedDate,
}: UserConsentRepresentation) => {
return <>{moment(lastUpdatedDate).format("MM/DD/YY hh:MM A")}</>;
};
return (
<>
<PageSection variant="light">
<KeycloakDataTable
loader={loader}
ariaLabelKey="roles:roleList"
columns={[
{
name: "clientId",
displayKey: "clients:Client",
cellFormatters: [emptyFormatter()],
transforms: [cellWidth(20)],
},
{
name: "grantedClientScopes",
displayKey: "client-scopes:grantedClientScopes",
cellFormatters: [emptyFormatter()],
cellRenderer: clientScopesRenderer,
transforms: [cellWidth(30)],
},
{
name: "createdDate",
displayKey: "clients:created",
cellFormatters: [emptyFormatter()],
cellRenderer: createdRenderer,
transforms: [cellWidth(20)],
},
{
name: "lastUpdatedDate",
displayKey: "clients:lastUpdated",
cellFormatters: [emptyFormatter()],
cellRenderer: lastUpdatedRenderer,
transforms: [cellWidth(20)],
},
]}
emptyState={
<ListEmptyState
hasIcon={true}
icon={CubesIcon}
message={t("users:noConsents")}
instructions={t("users:noConsentsText")}
onPrimaryAction={() => {}}
/>
}
/>
</PageSection>
</>
);
};

View file

@ -16,6 +16,7 @@ import { useAdminClient } from "../context/auth/AdminClient";
import { useHistory, useParams, useRouteMatch } from "react-router-dom"; import { useHistory, useParams, useRouteMatch } from "react-router-dom";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { UserGroups } from "./UserGroups"; import { UserGroups } from "./UserGroups";
import { UserConsents } from "./UserConsents";
export const UsersTabs = () => { export const UsersTabs = () => {
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
@ -24,7 +25,7 @@ export const UsersTabs = () => {
const history = useHistory(); const history = useHistory();
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const form = useForm<UserRepresentation>({ mode: "onChange" }); const userForm = useForm<UserRepresentation>({ mode: "onChange" });
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const [user, setUser] = useState(""); const [user, setUser] = useState("");
@ -69,7 +70,7 @@ export const UsersTabs = () => {
data-testid="user-details-tab" data-testid="user-details-tab"
title={<TabTitleText>{t("details")}</TabTitleText>} title={<TabTitleText>{t("details")}</TabTitleText>}
> >
<UserForm form={form} save={save} editMode={true} /> <UserForm form={userForm} save={save} editMode={true} />
</Tab> </Tab>
<Tab <Tab
eventKey="groups" eventKey="groups"
@ -78,9 +79,16 @@ export const UsersTabs = () => {
> >
<UserGroups /> <UserGroups />
</Tab> </Tab>
<Tab
eventKey="consents"
data-testid="user-consents-tab"
title={<TabTitleText>{t("users:consents")}</TabTitleText>}
>
<UserConsents />
</Tab>
</KeycloakTabs> </KeycloakTabs>
)} )}
{!id && <UserForm form={form} save={save} editMode={false} />} {!id && <UserForm form={userForm} save={save} editMode={false} />}
</PageSection> </PageSection>
</> </>
); );

View file

@ -52,7 +52,11 @@
"updatePassword": "Update Password", "updatePassword": "Update Password",
"updateProfile": "Update Profile", "updateProfile": "Update Profile",
"verifyEmail": "Verify Email", "verifyEmail": "Verify Email",
"updateUserLocale": "Update User Locale" "updateUserLocale": "Update User Locale",
"consents": "Consents",
"noConsents": "No consents",
"noConsentsText": "The consents will only be recorded when users try to access a client that is configured to require consent. In that case, users will get a consent page which asks them to grant access to the client."
} }
} }