diff --git a/src/common-messages.json b/src/common-messages.json index 6c016b8297..21e541efc9 100644 --- a/src/common-messages.json +++ b/src/common-messages.json @@ -11,6 +11,7 @@ "revert": "Revert", "cancel": "Cancel", "continue": "Continue", + "close": "Close", "delete": "Delete", "remove": "Remove", "search": "Search", diff --git a/src/components/confirm-dialog/ConfirmDialog.tsx b/src/components/confirm-dialog/ConfirmDialog.tsx index e9c0bfa285..ea0ea80318 100644 --- a/src/components/confirm-dialog/ConfirmDialog.tsx +++ b/src/components/confirm-dialog/ConfirmDialog.tsx @@ -35,6 +35,7 @@ export interface ConfirmDialogModalProps extends ConfirmDialogProps { export type ConfirmDialogProps = { titleKey: string; messageKey?: string; + noCancelButton?: boolean; cancelButtonLabel?: string; continueButtonLabel?: string; continueButtonVariant?: ButtonVariant; @@ -47,6 +48,7 @@ export type ConfirmDialogProps = { export const ConfirmDialogModal = ({ titleKey, messageKey, + noCancelButton, cancelButtonLabel, continueButtonLabel, continueButtonVariant, @@ -77,17 +79,19 @@ export const ConfirmDialogModal = ({ > {t(continueButtonLabel || "common:continue")} , - , + !noCancelButton && ( + + ), ]} > {!messageKey && children} diff --git a/src/components/table-toolbar/KeycloakDataTable.tsx b/src/components/table-toolbar/KeycloakDataTable.tsx index 9b476d6902..49cc4bf581 100644 --- a/src/components/table-toolbar/KeycloakDataTable.tsx +++ b/src/components/table-toolbar/KeycloakDataTable.tsx @@ -229,11 +229,13 @@ export function KeycloakDataTable({ return node.map(getNodeText).join(""); } if (typeof node === "object" && node) { - return getNodeText( - isValidElement((node as TitleCell).title) - ? (node as TitleCell).title.props.children - : (node as JSX.Element).props.children - ); + if ((node as TitleCell).title) { + return getNodeText( + isValidElement((node as TitleCell).title) + ? (node as TitleCell).title.props.children + : (node as JSX.Element).props.children + ); + } } return ""; }; diff --git a/src/groups/GroupTable.tsx b/src/groups/GroupTable.tsx index 9162e52596..f14ceadb9d 100644 --- a/src/groups/GroupTable.tsx +++ b/src/groups/GroupTable.tsx @@ -56,10 +56,21 @@ export const GroupTable = () => { return response ? response.length : 0; }; - const loader = async () => { + const loader = async (first?: number, max?: number, search?: string) => { + const params: { [name: string]: string | number } = { + first: first!, + max: max!, + }; + + const searchParam = search || ""; + + if (searchParam) { + params.search = searchParam; + } + const groupsData = id - ? (await adminClient.groups.findOne({ id })).subGroups - : await adminClient.groups.find(); + ? (await adminClient.groups.findOne({ id, ...params })).subGroups + : await adminClient.groups.find(params); if (groupsData) { const memberPromises = groupsData.map((group) => getMembers(group.id!)); @@ -121,6 +132,7 @@ export const GroupTable = () => { <> setSelectedRows([...rows])} canSelectAll={false} loader={loader} diff --git a/src/realm-settings/KeysListTab.tsx b/src/realm-settings/KeysListTab.tsx new file mode 100644 index 0000000000..e4f3055135 --- /dev/null +++ b/src/realm-settings/KeysListTab.tsx @@ -0,0 +1,180 @@ +import React, { useState } from "react"; +import { useHistory, useRouteMatch } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Button, ButtonVariant, PageSection } from "@patternfly/react-core"; +import { KeyMetadataRepresentation } from "keycloak-admin/lib/defs/keyMetadataRepresentation"; +import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; +import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; +import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; +import { emptyFormatter } from "../util"; +import ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation"; + +import "./RealmSettingsSection.css"; +import { cellWidth } from "@patternfly/react-table"; + +type KeyData = KeyMetadataRepresentation & { + provider?: string; +}; + +type KeysTabInnerProps = { + keys: KeyData[]; +}; + +export const KeysTabInner = ({ keys }: KeysTabInnerProps) => { + const { t } = useTranslation("roles"); + const history = useHistory(); + const { url } = useRouteMatch(); + const [key, setKey] = useState(0); + const refresh = () => setKey(new Date().getTime()); + + const [publicKey, setPublicKey] = useState(""); + const [certificate, setCertificate] = useState(""); + + const loader = async () => { + return keys; + }; + + React.useEffect(() => { + refresh(); + }, [keys]); + + const [togglePublicKeyDialog, PublicKeyDialog] = useConfirmDialog({ + titleKey: t("realm-settings:publicKeys").slice(0, -1), + messageKey: publicKey, + continueButtonLabel: "common:close", + continueButtonVariant: ButtonVariant.primary, + noCancelButton: true, + onConfirm: async () => {}, + }); + + const [toggleCertificateDialog, CertificateDialog] = useConfirmDialog({ + titleKey: t("realm-settings:certificate"), + messageKey: certificate, + continueButtonLabel: "common:close", + continueButtonVariant: ButtonVariant.primary, + noCancelButton: true, + onConfirm: async () => {}, + }); + + const goToCreate = () => history.push(`${url}/add-role`); + + const ProviderRenderer = ({ provider }: KeyData) => { + return <>{provider}; + }; + + const renderPublicKeyButton = (publicKey: string) => { + return ( + + ); + }; + + const ButtonRenderer = ({ provider, publicKey, certificate }: KeyData) => { + if (provider === "ecdsa-generated") { + return <>{renderPublicKeyButton(publicKey!)}; + } + if (provider === "rsa-generated" || provider === "fallback-RS256") { + return ( + <> +
+ {renderPublicKeyButton(publicKey!)} + +
+ + ); + } + }; + + return ( + <> + + + + + } + /> + + + ); +}; + +type KeysProps = { + keys: KeyMetadataRepresentation[]; + realmComponents: ComponentRepresentation[]; +}; + +export const KeysListTab = ({ keys, realmComponents, ...props }: KeysProps) => { + return ( + { + const provider = realmComponents.find( + (component: ComponentRepresentation) => + component.id === key.providerId + ); + return { ...key, provider: provider?.name }; + })} + {...props} + /> + ); +}; diff --git a/src/realm-settings/KeysTab.tsx b/src/realm-settings/KeysTab.tsx new file mode 100644 index 0000000000..fe93ca417a --- /dev/null +++ b/src/realm-settings/KeysTab.tsx @@ -0,0 +1,185 @@ +import React, { useState } from "react"; +import { useHistory, useRouteMatch } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { Button, ButtonVariant, PageSection } from "@patternfly/react-core"; +import { KeyMetadataRepresentation } from "keycloak-admin/lib/defs/keyMetadataRepresentation"; +import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; +import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; +import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; +import { emptyFormatter } from "../util"; +import ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation"; + +import "./RealmSettingsSection.css"; +import { cellWidth } from "@patternfly/react-table"; + +type KeyData = KeyMetadataRepresentation & { + provider?: string; +}; + +type KeysTabInnerProps = { + keys: KeyData[]; +}; + +export const KeysTabInner = ({ keys }: KeysTabInnerProps) => { + const { t } = useTranslation("roles"); + const history = useHistory(); + const { url } = useRouteMatch(); + const [key, setKey] = useState(0); + const refresh = () => setKey(new Date().getTime()); + + const [publicKey, setPublicKey] = useState(""); + const [certificate, setCertificate] = useState(""); + + const loader = async () => { + return keys; + }; + + React.useEffect(() => { + refresh(); + }, [keys]); + + const [togglePublicKeyDialog, PublicKeyDialog] = useConfirmDialog({ + titleKey: t("realm-settings:publicKeys").slice(0, -1), + messageKey: publicKey, + continueButtonLabel: "common:close", + continueButtonVariant: ButtonVariant.primary, + noCancelButton: true, + onConfirm: async () => {}, + }); + + const [toggleCertificateDialog, CertificateDialog] = useConfirmDialog({ + titleKey: t("realm-settings:certificate"), + messageKey: certificate, + continueButtonLabel: "common:close", + continueButtonVariant: ButtonVariant.primary, + noCancelButton: true, + onConfirm: async () => {}, + }); + + const goToCreate = () => history.push(`${url}/add-role`); + + const ProviderRenderer = ({ provider }: KeyData) => { + return <>{provider}; + }; + + const ButtonRenderer = ({ provider, publicKey, certificate }: KeyData) => { + if (provider === "ecdsa-generated") { + return ( + <> + + + ); + } + if (provider === "rsa-generated" || provider === "fallback-RS256") { + return ( + <> + + + + ); + } + }; + + return ( + <> + + + + + } + /> + + + ); +}; + +type KeysProps = { + keys: KeyMetadataRepresentation[]; + realmComponents: ComponentRepresentation[]; +}; + +export const KeysTab = ({ keys, realmComponents, ...props }: KeysProps) => { + return ( + { + const provider = realmComponents.find( + (component: ComponentRepresentation) => + component.id === key.providerId + ); + return { ...key, provider: provider?.providerId }; + })} + {...props} + /> + ); +}; diff --git a/src/realm-settings/RealmSettingsSection.css b/src/realm-settings/RealmSettingsSection.css index e7732d2dce..b14a033e39 100644 --- a/src/realm-settings/RealmSettingsSection.css +++ b/src/realm-settings/RealmSettingsSection.css @@ -16,3 +16,6 @@ div.pf-c-card__body.kc-form-panel__body { padding-left: 0px; padding-bottom: var(--pf-global--spacer--2xl); } +button#kc-certificate.pf-c-button.pf-m-secondary { + margin-left: var(--pf-global--spacer--md); +} diff --git a/src/realm-settings/RealmSettingsSection.tsx b/src/realm-settings/RealmSettingsSection.tsx index 003aaa46f9..da28c0e702 100644 --- a/src/realm-settings/RealmSettingsSection.tsx +++ b/src/realm-settings/RealmSettingsSection.tsx @@ -10,6 +10,7 @@ import { DropdownSeparator, PageSection, Tab, + Tabs, TabTitleText, } from "@patternfly/react-core"; @@ -26,6 +27,9 @@ import { RealmSettingsGeneralTab } from "./GeneralTab"; import { PartialImportDialog } from "./PartialImport"; import { RealmSettingsThemesTab } from "./ThemesTab"; import { RealmSettingsEmailTab } from "./EmailTab"; +import { KeysListTab } from "./KeysListTab"; +import { KeyMetadataRepresentation } from "keycloak-admin/lib/defs/keyMetadataRepresentation"; +import ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation"; type RealmSettingsHeaderProps = { onChange: (value: boolean) => void; @@ -126,6 +130,11 @@ export const RealmSettingsSection = () => { const form = useForm(); const { control, getValues, setValue } = form; const [realm, setRealm] = useState(); + const [activeTab2, setActiveTab2] = useState(0); + const [keys, setKeys] = useState([]); + const [realmComponents, setRealmComponents] = useState< + ComponentRepresentation[] + >([]); useEffect(() => { return asyncStateFetch( @@ -138,6 +147,21 @@ export const RealmSettingsSection = () => { ); }, []); + useEffect(() => { + const update = async () => { + const keysMetaData = await adminClient.realms.getKeys({ + realm: realmName, + }); + setKeys(keysMetaData.keys!); + const realmComponents = await adminClient.components.find({ + type: "org.keycloak.keys.KeyProvider", + realm: realmName, + }); + setRealmComponents(realmComponents); + }; + setTimeout(update, 100); + }, []); + const setupForm = (realm: RealmRepresentation) => { Object.entries(realm).map((entry) => setValue(entry[0], entry[1])); }; @@ -207,6 +231,24 @@ export const RealmSettingsSection = () => { reset={() => setupForm(realm!)} /> + {t("realm-settings:keys")}} + data-testid="rs-keys-tab" + > + setActiveTab2(key as number)} + > + {t("keysList")}} + > + + + + diff --git a/src/realm-settings/messages.json b/src/realm-settings/messages.json index 1608450234..0c8132169e 100644 --- a/src/realm-settings/messages.json +++ b/src/realm-settings/messages.json @@ -30,6 +30,16 @@ "enableStartTLS": "Enable StartTLS", "username": "Username", "password": "Password", + "keys": "Keys", + "keysList": "Keys list", + "searchKey":"Search key", + "providers": "Providers", + "algorithm": "Algorithm", + "type": "Type", + "kid": "Kid", + "provider": "Provider", + "publicKeys": "Public keys", + "certificate": "Certificate", "userRegistration": "User registration", "userRegistrationHelpText": "Enable/disable the registration page. A link for registration will show on login page too.", "forgotPassword": "Forgot password",