Merge pull request #691 from jenny-s51/consentsUIFixes

Consents UI fixes
This commit is contained in:
mfrances17 2021-06-10 12:02:59 -04:00 committed by GitHub
commit 04bdf4bfa5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 176 additions and 53 deletions

View file

@ -1,7 +1,12 @@
import React from "react"; import React, { useState } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { PageSection } from "@patternfly/react-core"; import {
AlertVariant,
ButtonVariant,
Chip,
ChipGroup,
} from "@patternfly/react-core";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { emptyFormatter } from "../util"; import { emptyFormatter } from "../util";
@ -11,9 +16,16 @@ import _ from "lodash";
import type UserConsentRepresentation from "keycloak-admin/lib/defs/userConsentRepresentation"; import type UserConsentRepresentation from "keycloak-admin/lib/defs/userConsentRepresentation";
import { CubesIcon } from "@patternfly/react-icons"; import { CubesIcon } from "@patternfly/react-icons";
import moment from "moment"; import moment from "moment";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../components/alert/Alerts";
export const UserConsents = () => { export const UserConsents = () => {
const [selectedClient, setSelectedClient] = useState<
UserConsentRepresentation
>();
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
const { addAlert } = useAlerts();
const [key, setKey] = useState(0);
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
@ -21,16 +33,31 @@ export const UserConsents = () => {
return _.sortBy(consentsList, (client) => client.clientId?.toUpperCase()); return _.sortBy(consentsList, (client) => client.clientId?.toUpperCase());
}; };
const loader = async () => { const refresh = () => setKey(new Date().getTime());
const consents = await adminClient.users.listConsents({ id });
return alphabetize(consents); const loader = async () => {
const getConsents = await adminClient.users.listConsents({ id });
return alphabetize(getConsents);
}; };
const clientScopesRenderer = ({ const clientScopesRenderer = ({
grantedClientScopes, grantedClientScopes,
}: UserConsentRepresentation) => { }: UserConsentRepresentation) => {
return <>{grantedClientScopes!.join(", ")}</>; return (
<ChipGroup className="kc-consents-chip-group">
{grantedClientScopes!.map((currentChip) => (
<Chip
key={currentChip}
isReadOnly
className="kc-consents-chip"
id="consents-chip-text"
>
{currentChip}
</Chip>
))}
</ChipGroup>
);
}; };
const createdRenderer = ({ createDate }: UserConsentRepresentation) => { const createdRenderer = ({ createDate }: UserConsentRepresentation) => {
@ -43,52 +70,85 @@ export const UserConsents = () => {
return <>{moment(lastUpdatedDate).format("MM/DD/YY hh:MM A")}</>; return <>{moment(lastUpdatedDate).format("MM/DD/YY hh:MM A")}</>;
}; };
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "users:revokeClientScopesTitle",
messageKey: t("users:revokeClientScopes", {
clientId: selectedClient?.clientId,
}),
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.users.revokeConsent({
id,
clientId: selectedClient!.clientId!,
});
refresh();
addAlert(t("deleteGrantsSuccess"), AlertVariant.success);
} catch (error) {
addAlert(t("deleteGrantsError", { error }), AlertVariant.danger);
}
},
});
return ( return (
<> <>
<PageSection variant="light"> <DeleteConfirm />
<KeycloakDataTable <KeycloakDataTable
loader={loader} loader={loader}
ariaLabelKey="roles:roleList" key={key}
columns={[ ariaLabelKey="roles:roleList"
{ searchPlaceholderKey=" "
name: "clientId", columns={[
displayKey: "clients:Client", {
cellFormatters: [emptyFormatter()], name: "clientId",
transforms: [cellWidth(20)], 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(10)],
},
]}
actions={[
{
title: t("users:revoke"),
onRowClick: (client) => {
setSelectedClient(client);
toggleDeleteDialog();
}, },
{ },
name: "grantedClientScopes", ]}
displayKey: "client-scopes:grantedClientScopes", emptyState={
cellFormatters: [emptyFormatter()], <ListEmptyState
cellRenderer: clientScopesRenderer, hasIcon={true}
transforms: [cellWidth(30)], icon={CubesIcon}
}, message={t("users:noConsents")}
{ instructions={t("users:noConsentsText")}
name: "createdDate", onPrimaryAction={() => {}}
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

@ -108,9 +108,7 @@ export const UsersTabs = () => {
data-testid="user-consents-tab" data-testid="user-consents-tab"
title={<TabTitleText>{t("users:consents")}</TabTitleText>} title={<TabTitleText>{t("users:consents")}</TabTitleText>}
> >
<PageSection variant="light"> <UserConsents />
<UserConsents />
</PageSection>
</Tab> </Tab>
</KeycloakTabs> </KeycloakTabs>
)} )}

View file

@ -60,6 +60,11 @@
"noConsents": "No 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.", "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.",
"whoWillAppearLinkText": "Who will appear in this group list?", "whoWillAppearLinkText": "Who will appear in this group list?",
"whoWillAppearPopoverText": "Groups are hierarchical. When you select Direct Membership, you see only the child group that the user joined. Ancestor groups are not included." "whoWillAppearPopoverText": "Groups are hierarchical. When you select Direct Membership, you see only the child group that the user joined. Ancestor groups are not included.",
"revoke": "Revoke",
"revokeClientScopesTitle": "Revoke all granted client scopes?",
"revokeClientScopes": "Are you sure you want to revoke all granted client scopes for {{clientId}}?",
"deleteGrantsSuccess": "Grants successfully revoked.",
"deleteGrantsError": "Error deleting grants."
} }
} }

View file

@ -45,3 +45,63 @@ td.pf-c-table__check > input {
width: 20px; width: 20px;
vertical-align: text-top; vertical-align: text-top;
} }
.pf-c-toolbar__content-section {
margin-bottom: calc(var(--pf-global--spacer--lg) * -1);
}
.pf-c-chip.kc-consents-chip::before {
padding: 0px;
border: 0;
}
.pf-c-chip.kc-consents-chip {
padding: 0px;
}
.pf-c-chip-group__list-item {
margin-right: 0px;
}
#consents-chip-text.pf-c-chip-text {
font-size: var(--pf-c-table--cell--FontSize);
}
.kc-consents-chip > .pf-c-chip__text {
font-size: var(--pf-c-table--cell--FontSize);
display: inline-block;
}
.pf-c-chip-group.kc-consents-chip-group
> div.pf-c-chip-group__main
> ul.pf-c-chip-group__list
li:first-child
.pf-c-chip__text::before {
content: "";
}
.pf-c-chip-group.kc-consents-chip-group
> div.pf-c-chip-group__main
> ul.pf-c-chip-group__list
.pf-c-chip__text::before {
content: ", ";
}
div.pf-c-chip-group.kc-consents-chip-group
> div.pf-c-chip-group__main
> ul.pf-c-chip-group__list
> li.pf-c-chip-group__list-item:last-child
> button.pf-c-chip.pf-m-overflow::before {
content: "";
margin-left: var(--pf-global--spacer--sm);
}
div.pf-c-chip-group.kc-consents-chip-group
> div.pf-c-chip-group__main
> ul.pf-c-chip-group__list
> li.pf-c-chip-group__list-item:last-child
> button.pf-c-chip.pf-m-overflow
> span::before {
content: "";
margin-left: var(--pf-global--spacer--sm);
}