8000 show/update credential user attributes in admin console and account console by graziang · Pull Request #49218 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 84 additions & 54 deletions js/apps/account-ui/src/verifiable-credentials/CredentialRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
FlexItem,
} from "@patternfly/react-core";
import { ExternalLinkAltIcon } from "@patternfly/react-icons";
import { useState } from "react";
import { useTranslation } from "react-i18next";

import { deleteVerifiableCredential } from "../api/methods";
import { UserVerifiableCredentialRepresentation } from "../api/representations";
import { formatDate, FORMAT_DATE_ONLY } from "../utils/formatDate";
import { useAccountAlerts } from "../utils/useAccountAlerts";
import { UserAttributesDialog } from "./UserAttributesDialog";

type CredentialRowProps = {
credential: UserVerifiableCredentialRepresentation;
Expand All @@ -29,6 +31,11 @@ export const CredentialRow = ({ credential, refresh }: CredentialRowProps) => {
const { t } = useTranslation();
const context = useEnvironment();
const { addAlert, addError } = useAccountAlerts();
const [showAttributesDialog, setShowAttributesDialog] = useState(false);

const hasUserAttributes =
credential.userAttributes != null &&
Object.keys(credential.userAttributes).length > 0;

const hasManageRole = () => {
const token = context.keycloak.tokenParsed;
Expand Down Expand Up @@ -73,63 +80,86 @@ export const CredentialRow = ({ credential, refresh }: CredentialRowProps) => {
};

return (
<DataListItem
id={`credential-${credential.credentialScopeName}`}
key={credential.credentialScopeName}
aria-label={t("verifiableCredentials")}
>
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key="name" width={2}>
{credential.credentialScopeName}
</DataListCell>,
<DataListCell key="created" width={2}>
{credential.createdDate
? formatDate(
new Date(credential.createdDate),
undefined,
FORMAT_DATE_ONLY,
)
: "—"}
</DataListCell>,
]}
<>
{showAttributesDialog && hasUserAttributes && (
<UserAttributesDialog
credentialScopeName={credential.credentialScopeName!}
userAttributes={credential.userAttributes!}
=> setShowAttributesDialog(false)}
/>
<DataListAction
aria-labelledby={t("actions")}
aria-label={t("credentialActions")}
id="credentialActions"
>
<Flex>
<FlexItem>
<Button
id={`credential-${credential.credentialScopeName}-issue`}
variant="link"
>
icon={<ExternalLinkAltIcon />}
>
{t("issueToWallet")}
</Button>
</FlexItem>
{hasManageRole() && (
)}
<DataListItem
id={`credential-${credential.credentialScopeName}`}
key={credential.credentialScopeName}
aria-label={t("verifiableCredentials")}
>
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key="name" width={2}>
{credential.credentialScopeName}
</DataListCell>,
<DataListCell key="created" width={2}>
{credential.createdDate
? formatDate(
new Date(credential.createdDate),
undefined,
FORMAT_DATE_ONLY,
)
: "—"}
</DataListCell>,
<DataListCell key="attributes" width={2}>
{hasUserAttributes ? (
<Button
variant="link"
=> setShowAttributesDialog(true)}
>
{t("credentialViewAttributes")}
</Button>
) : (
<span className="pf-v5-u-color-200">
{t("credentialNoUserAttributes")}
</span>
)}
</DataListCell>,
]}
/>
<DataListAction
aria-labelledby={t("actions")}
aria-label={t("credentialActions")}
id="credentialActions"
>
<Flex>
<FlexItem>
<ContinueCancelModal
buttonTitle={t("delete")}
modalTitle={t("deleteCredential")}
continueLabel={t("delete")}
cancelLabel={t("cancel")}
buttonVariant="link"
>
<Button
id={`credential-${credential.credentialScopeName}-issue`}
variant="link"
>
icon={<ExternalLinkAltIcon />}
>
{t("deleteCredentialConfirm", {
credentialName: credential.credentialScopeName,
})}
</ContinueCancelModal>
{t("issueToWallet")}
</Button>
</FlexItem>
)}
</Flex>
</DataListAction>
</DataListItemRow>
</DataListItem>
{hasManageRole() && (
<FlexItem>
<ContinueCancelModal
buttonTitle={t("delete")}
modalTitle={t("deleteCredential")}
continueLabel={t("delete")}
cancelLabel={t("cancel")}
buttonVariant="link"
>
>
{t("deleteCredentialConfirm", {
credentialName: credential.credentialScopeName,
})}
</ContinueCancelModal>
</FlexItem>
)}
</Flex>
</DataListAction>
</DataListItemRow>
</DataListItem>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useTranslation } from "react-i18next";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can move this to the ui-shared module so that it doesn't need to be copied

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about it, but with the next updates I think they might change between admin console and admin ui, I would prefer to keep them separate

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 , those may very likely differ.

For me, current PR is OK.

@edewit @graziang Or it can be moved to shared module and then create separate components for admin/account in some follow-up PRs if needed? What is the preferred way here?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so when the component is moved to the ui-shared module it can be used by account and by admin-ui, it's already setup this way so all that is needed it to move it (export it in the module) and change the imports

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but if they are going to be different anyway we can leave it like this as well

import { Modal, ModalVariant } from "@patternfly/react-core";
import {
Table,
TableVariant,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@patternfly/react-table";

type UserAttributesDialogProps = {
credentialScopeName: string;
userAttributes: Record<string, string[]>;
onClose: () => void;
};

export const UserAttributesDialog = ({
credentialScopeName,
userAttributes,
onClose,
}: UserAttributesDialogProps) => {
const { t } = useTranslation();

return (
<Modal
variant={ModalVariant.medium}
title={t("credentialUserAttributesFor", { credentialScopeName })}
isOpen
>
>
<Table
aria-label={t("credentialUserAttributes")}
variant={TableVariant.compact}
>
<Thead>
<Tr>
<Th>{t("credentialAttributeName")}</Th>
<Th>{t("credentialAttributeValue")}</Th>
</Tr>
</Thead>
<Tbody>
{Object.entries(userAttributes).map(([key, values]) => (
<Tr key={key}>
<Td>{key}</Td>
<Td>{values.join(", ")}</Td>
</Tr>
))}
</Tbody>
</Table>
</Modal>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export const VerifiableCredentials = () => {
<DataListCell key="credential-created-header" width={2}>
<strong>{t("credentialCreatedDate")}</strong>
</DataListCell>,
<DataListCell key="credential-attributes-header" width={2}>
<strong>{t("userAttributes")}</strong>
</DataListCell>,
]}
/>
</DataListItemRow>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
cancel=Cancel
update=Update
deleteConfirm_other=Are you sure you want to delete these groups?
trusted-hosts.label=Trusted Hosts
registration-web-origins.label=Allowed Registration Web Origins
Expand Down Expand Up @@ -3595,6 +3596,17 @@ revokeVerifiableCredentialTitle=Revoke verifiable credential?
revokeVerifiableCredentialConfirm=Are you sure you want to revoke the verifiable credential "{{credentialScopeName}}" from this user?
revokeVerifiableCredentialSuccess=Verifiable credential successfully revoked.
revokeVerifiableCredentialError=Could not revoke verifiable credential\: {{error}}
updateVerifiableCredentialTitle=Update verifiable credential?
updateVerifiableCredentialConfirm=Are you sure you want to update the verifiable credential "{{credentialScopeName}}"? This will refresh the user attributes snapshot and increment the revision.
updateVerifiableCredentialSuccess=Verifiable credential successfully updated.
updateVerifiableCredentialError=Could not update verifiable credential\: {{error}}
updateCredential=Update credential
credentialUserAttributes=User attributes
credentialNoUserAttributes=No user attributes
credentialViewAttributes=View attributes
credentialUserAttributesFor=User attributes for "{{credentialScopeName}}"
credentialAttributeName=Attribute name
credentialAttributeValue=Attribute value
selectCredentialScope=Select a credential scope
noOid4vcScopesAvailable=No OID4VC client scopes are available in this realm. Create OID4VC client scopes first.
organizations=Organizations
Expand Down
54 changes: 54 additions & 0 deletions js/apps/admin-ui/src/user/UserAttributesDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useTranslation } from "react-i18next";
import { Modal, ModalVariant } from "@patternfly/react-core";
import {
Table,
TableVariant,
Tbody,
Td,
Th,
Thead,
Tr,
} from "@patternfly/react-table";

type UserAttributesDialogProps = {
credentialScopeName: string;
userAttributes: Record<string, string[]>;
onClose: () => void;
};

export const UserAttributesDialog = ({
credentialScopeName,
userAttributes,
onClose,
}: UserAttributesDialogProps) => {
const { t } = useTranslation();

return (
<Modal
variant={ModalVariant.medium}
title={t("credentialUserAttributesFor", { credentialScopeName })}
isOpen
>
>
<Table
aria-label={t("credentialUserAttributes")}
variant={TableVariant.compact}
>
<Thead>
<Tr>
<Th>{t("credentialAttributeName")}</Th>
<Th>{t("credentialAttributeValue")}</Th>
</Tr>
</Thead>
<Tbody>
{Object.entries(userAttributes).map(([key, values]) => (
<Tr key={key}>
<Td>{key}</Td>
<Td>{values.join(", ")}</Td>
</Tr>
))}
</Tbody>
</Table>
</Modal>
);
};
Loading
Loading
0