From 2bcdf510752612ee64954e99885f1e12c7ddf625 Mon Sep 17 00:00:00 2001 From: Eugenia <32821331+jenny-s51@users.noreply.github.com> Date: Wed, 14 Apr 2021 14:39:21 -0400 Subject: [PATCH] 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 --- cypress/integration/users_test.spec.ts | 13 ++- src/client-scopes/messages.json | 1 + src/clients/ClientsSection.tsx | 1 + src/clients/messages.json | 2 + .../list-empty-state/ListEmptyState.tsx | 5 +- .../table-toolbar/KeycloakDataTable.tsx | 4 + src/user/UserConsents.tsx | 94 +++++++++++++++++++ src/user/UsersTabs.tsx | 14 ++- src/user/messages.json | 6 +- 9 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 src/user/UserConsents.tsx diff --git a/cypress/integration/users_test.spec.ts b/cypress/integration/users_test.spec.ts index 0ad74eaede..5f89dc7201 100644 --- a/cypress/integration/users_test.spec.ts +++ b/cypress/integration/users_test.spec.ts @@ -126,8 +126,19 @@ describe("Users test", () => { cy.getId("modalConfirm").click(); }); - it("Delete user", function () { + it("Go to user consents test", function () { + cy.wait(1000); 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 cy.wait(1000); listingPage.deleteItem(itemId); diff --git a/src/client-scopes/messages.json b/src/client-scopes/messages.json index 8691815e0a..34d745383f 100644 --- a/src/client-scopes/messages.json +++ b/src/client-scopes/messages.json @@ -2,6 +2,7 @@ "client-scopes": { "createClientScope": "Create client scope", "clientScopeList": "Client scopes", + "grantedClientScopes": "Granted client scopes", "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", "searchFor": "Search for client scope", diff --git a/src/clients/ClientsSection.tsx b/src/clients/ClientsSection.tsx index 7b0e808941..4f255f8ddf 100644 --- a/src/clients/ClientsSection.tsx +++ b/src/clients/ClientsSection.tsx @@ -99,6 +99,7 @@ export const ClientsSection = () => { } loader={loader} isPaginated ariaLabelKey="clients:clientList" diff --git a/src/clients/messages.json b/src/clients/messages.json index 2a9c7d9fab..699d30912a 100644 --- a/src/clients/messages.json +++ b/src/clients/messages.json @@ -83,6 +83,8 @@ "tokenDeleteSuccess": "initial access token created successfully", "tokenDeleteError": "Could not delete initial access token: '{{error}}'", "timestamp": "Created date", + "created": "Created", + "lastUpdated": "Last updated", "expires": "Expires", "count": "Count", "remainingCount": "Remaining count", diff --git a/src/components/list-empty-state/ListEmptyState.tsx b/src/components/list-empty-state/ListEmptyState.tsx index 6ffc94b407..fb4dbe4666 100644 --- a/src/components/list-empty-state/ListEmptyState.tsx +++ b/src/components/list-empty-state/ListEmptyState.tsx @@ -8,6 +8,7 @@ import { ButtonVariant, EmptyStateSecondaryActions, } from "@patternfly/react-core"; +import { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon"; import { PlusCircleIcon } from "@patternfly/react-icons"; import { SearchIcon } from "@patternfly/react-icons"; @@ -23,6 +24,7 @@ export type ListEmptyStateProps = { primaryActionText?: string; onPrimaryAction?: MouseEventHandler; hasIcon?: boolean; + icon?: React.ComponentClass; isSearchVariant?: boolean; secondaryActions?: Action[]; }; @@ -35,6 +37,7 @@ export const ListEmptyState = ({ isSearchVariant, primaryActionText, secondaryActions, + icon, }: ListEmptyStateProps) => { return ( <> @@ -42,7 +45,7 @@ export const ListEmptyState = ({ {hasIcon && isSearchVariant ? ( ) : ( - hasIcon && + hasIcon && )} {message} diff --git a/src/components/table-toolbar/KeycloakDataTable.tsx b/src/components/table-toolbar/KeycloakDataTable.tsx index 8ca1687202..fee52c73b2 100644 --- a/src/components/table-toolbar/KeycloakDataTable.tsx +++ b/src/components/table-toolbar/KeycloakDataTable.tsx @@ -18,6 +18,7 @@ import _ from "lodash"; import { PaginatingTableToolbar } from "./PaginatingTableToolbar"; import { asyncStateFetch } from "../../context/auth/AdminClient"; import { ListEmptyState } from "../list-empty-state/ListEmptyState"; +import { SVGIconProps } from "@patternfly/react-icons/dist/js/createIcon"; type Row<T> = { data: T; @@ -98,6 +99,7 @@ export type DataListProps<T> = { searchTypeComponent?: ReactNode; toolbarItem?: ReactNode; emptyState?: ReactNode; + icon?: React.ComponentClass<SVGIconProps>; }; /** @@ -136,6 +138,7 @@ export function KeycloakDataTable<T>({ searchTypeComponent, toolbarItem, emptyState, + icon, ...props }: DataListProps<T>) { const { t } = useTranslation(); @@ -330,6 +333,7 @@ export function KeycloakDataTable<T>({ searchPlaceholderKey && ( <ListEmptyState hasIcon={true} + icon={icon} isSearchVariant={true} message={t("noSearchResults")} instructions={t("noSearchResultsInstructions")} diff --git a/src/user/UserConsents.tsx b/src/user/UserConsents.tsx new file mode 100644 index 0000000000..7a0d15172b --- /dev/null +++ b/src/user/UserConsents.tsx @@ -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> + </> + ); +}; diff --git a/src/user/UsersTabs.tsx b/src/user/UsersTabs.tsx index 56935b3fbe..bb23e04b61 100644 --- a/src/user/UsersTabs.tsx +++ b/src/user/UsersTabs.tsx @@ -16,6 +16,7 @@ import { useAdminClient } from "../context/auth/AdminClient"; import { useHistory, useParams, useRouteMatch } from "react-router-dom"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import { UserGroups } from "./UserGroups"; +import { UserConsents } from "./UserConsents"; export const UsersTabs = () => { const { t } = useTranslation("roles"); @@ -24,7 +25,7 @@ export const UsersTabs = () => { const history = useHistory(); const adminClient = useAdminClient(); - const form = useForm<UserRepresentation>({ mode: "onChange" }); + const userForm = useForm<UserRepresentation>({ mode: "onChange" }); const { id } = useParams<{ id: string }>(); const [user, setUser] = useState(""); @@ -69,7 +70,7 @@ export const UsersTabs = () => { data-testid="user-details-tab" title={<TabTitleText>{t("details")}</TabTitleText>} > - <UserForm form={form} save={save} editMode={true} /> + <UserForm form={userForm} save={save} editMode={true} /> </Tab> <Tab eventKey="groups" @@ -78,9 +79,16 @@ export const UsersTabs = () => { > <UserGroups /> </Tab> + <Tab + eventKey="consents" + data-testid="user-consents-tab" + title={<TabTitleText>{t("users:consents")}</TabTitleText>} + > + <UserConsents /> + </Tab> </KeycloakTabs> )} - {!id && <UserForm form={form} save={save} editMode={false} />} + {!id && <UserForm form={userForm} save={save} editMode={false} />} </PageSection> </> ); diff --git a/src/user/messages.json b/src/user/messages.json index 14c0ce5ca4..9269dac8c1 100644 --- a/src/user/messages.json +++ b/src/user/messages.json @@ -52,7 +52,11 @@ "updatePassword": "Update Password", "updateProfile": "Update Profile", "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." + } }