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