diff --git a/src/user/UserConsents.tsx b/src/user/UserConsents.tsx index fab50c408b..ac653ac4ff 100644 --- a/src/user/UserConsents.tsx +++ b/src/user/UserConsents.tsx @@ -1,7 +1,12 @@ -import React from "react"; +import React, { useState } from "react"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { Chip, ChipGroup } from "@patternfly/react-core"; +import { + AlertVariant, + ButtonVariant, + Chip, + ChipGroup, +} from "@patternfly/react-core"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { emptyFormatter } from "../util"; @@ -11,9 +16,16 @@ import _ from "lodash"; import type UserConsentRepresentation from "keycloak-admin/lib/defs/userConsentRepresentation"; import { CubesIcon } from "@patternfly/react-icons"; import moment from "moment"; +import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; +import { useAlerts } from "../components/alert/Alerts"; export const UserConsents = () => { + const [selectedClient, setSelectedClient] = useState< + UserConsentRepresentation + >(); const { t } = useTranslation("roles"); + const { addAlert } = useAlerts(); + const [key, setKey] = useState(0); const adminClient = useAdminClient(); const { id } = useParams<{ id: string }>(); @@ -21,6 +33,8 @@ export const UserConsents = () => { return _.sortBy(consentsList, (client) => client.clientId?.toUpperCase()); }; + const refresh = () => setKey(new Date().getTime()); + const loader = async () => { const getConsents = await adminClient.users.listConsents({ id }); @@ -56,10 +70,34 @@ export const UserConsents = () => { return <>{moment(lastUpdatedDate).format("MM/DD/YY hh:MM A")}; }; + const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ + titleKey: "users:revokeClientScopesTitle", + messageKey: t("users:revokeClientScopes") + selectedClient?.clientId + "?", + continueButtonLabel: "common:delete", + continueButtonVariant: ButtonVariant.danger, + onConfirm: async () => { + try { + await adminClient.users.revokeConsent({ + id, + // realm: realm, + clientId: selectedClient!.clientId!, + }); + + refresh(); + + addAlert(t("deleteGrantsSuccess"), AlertVariant.success); + } catch (error) { + addAlert(t("deleteGrantsError", { error }), AlertVariant.danger); + } + }, + }); + return ( <> + { displayKey: "clients:lastUpdated", cellFormatters: [emptyFormatter()], cellRenderer: lastUpdatedRenderer, - transforms: [cellWidth(20)], + transforms: [cellWidth(10)], + }, + ]} + actions={[ + { + title: t("users:revoke"), + onRowClick: (client) => { + setSelectedClient(client); + toggleDeleteDialog(); + }, }, ]} emptyState={ diff --git a/src/user/messages.json b/src/user/messages.json index 17d55ec009..2b794844ed 100644 --- a/src/user/messages.json +++ b/src/user/messages.json @@ -60,6 +60,11 @@ "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.", "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 ", + "deleteGrantsSuccess": "Grants successfully revoked.", + "deleteGrantsError": "Error deleting grants." } } diff --git a/src/user/user-section.css b/src/user/user-section.css index 8273cb9ddc..f601d2980d 100644 --- a/src/user/user-section.css +++ b/src/user/user-section.css @@ -87,12 +87,21 @@ td.pf-c-table__check > input { content: ", "; } -.pf-c-chip-group.kc-consents-chip-group +div.pf-c-chip-group.kc-consents-chip-group > div.pf-c-chip-group__main > ul.pf-c-chip-group__list - .pf-m-overflow - .pf-c-chip__text::before { + > 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); - padding-left: 0px; }