Merge pull request #585 from jenny-s51/rsKeyProviders

Realm settings(keys): add providers sub-tab
This commit is contained in:
mfrances17 2021-05-20 11:44:48 -04:00 committed by GitHub
commit 0c6e1620fd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 341 additions and 209 deletions

View file

@ -47,6 +47,7 @@ type DataTableProps<T> = {
onSelect?: (isSelected: boolean, rowIndex: number) => void; onSelect?: (isSelected: boolean, rowIndex: number) => void;
onCollapse?: (isOpen: boolean, rowIndex: number) => void; onCollapse?: (isOpen: boolean, rowIndex: number) => void;
canSelectAll: boolean; canSelectAll: boolean;
isNotCompact?: boolean;
}; };
function DataTable<T>({ function DataTable<T>({
@ -58,13 +59,14 @@ function DataTable<T>({
onSelect, onSelect,
onCollapse, onCollapse,
canSelectAll, canSelectAll,
isNotCompact,
...props ...props
}: DataTableProps<T>) { }: DataTableProps<T>) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<Table <Table
{...props} {...props}
variant={TableVariant.compact} variant={isNotCompact ? undefined : TableVariant.compact}
onSelect={ onSelect={
onSelect onSelect
? (_, isSelected, rowIndex) => onSelect(isSelected, rowIndex) ? (_, isSelected, rowIndex) => onSelect(isSelected, rowIndex)
@ -124,6 +126,7 @@ export type DataListProps<T> = {
toolbarItem?: ReactNode; toolbarItem?: ReactNode;
emptyState?: ReactNode; emptyState?: ReactNode;
icon?: React.ComponentClass<SVGIconProps>; icon?: React.ComponentClass<SVGIconProps>;
isNotCompact?: boolean;
}; };
/** /**
@ -155,6 +158,7 @@ export function KeycloakDataTable<T>({
isPaginated = false, isPaginated = false,
onSelect, onSelect,
canSelectAll = false, canSelectAll = false,
isNotCompact,
detailColumns, detailColumns,
isRowDisabled, isRowDisabled,
loader, loader,
@ -383,6 +387,7 @@ export function KeycloakDataTable<T>({
actionResolver={actionResolver} actionResolver={actionResolver}
rows={filteredData || rows} rows={filteredData || rows}
columns={columns} columns={columns}
isNotCompact={isNotCompact}
ariaLabelKey={ariaLabelKey} ariaLabelKey={ariaLabelKey}
/> />
)} )}

View file

@ -14,6 +14,7 @@ import { cellWidth } from "@patternfly/react-table";
type KeyData = KeyMetadataRepresentation & { type KeyData = KeyMetadataRepresentation & {
provider?: string; provider?: string;
type?: string;
}; };
type KeysTabInnerProps = { type KeysTabInnerProps = {
@ -62,30 +63,36 @@ export const KeysTabInner = ({ keys }: KeysTabInnerProps) => {
return <>{provider}</>; return <>{provider}</>;
}; };
const renderPublicKeyButton = (publicKey: string) => { const ButtonRenderer = ({ type, publicKey, certificate }: KeyData) => {
return ( if (type === "EC") {
<Button
onClick={() => {
togglePublicKeyDialog();
setPublicKey(publicKey!);
}}
variant="secondary"
id="kc-public-key"
>
{t("realm-settings:publicKeys").slice(0, -1)}
</Button>
);
};
const ButtonRenderer = ({ provider, publicKey, certificate }: KeyData) => {
if (provider === "ecdsa-generated") {
return <>{renderPublicKeyButton(publicKey!)}</>;
}
if (provider === "rsa-generated" || provider === "fallback-RS256") {
return ( return (
<> <>
<div> <Button
{renderPublicKeyButton(publicKey!)} onClick={() => {
togglePublicKeyDialog();
setPublicKey(publicKey!);
}}
variant="secondary"
id="kc-public-key"
>
{t("realm-settings:publicKeys").slice(0, -1)}
</Button>
</>
);
} else if (type === "RSA") {
return (
<>
<div className="button-wrapper">
<Button
onClick={() => {
togglePublicKeyDialog();
setPublicKey(publicKey!);
}}
variant="secondary"
id="kc-rsa-public-key"
>
{t("realm-settings:publicKeys").slice(0, -1)}
</Button>
<Button <Button
onClick={() => { onClick={() => {
toggleCertificateDialog(); toggleCertificateDialog();
@ -109,6 +116,7 @@ export const KeysTabInner = ({ keys }: KeysTabInnerProps) => {
<CertificateDialog /> <CertificateDialog />
<KeycloakDataTable <KeycloakDataTable
key={key} key={key}
isNotCompact={true}
loader={loader} loader={loader}
ariaLabelKey="realm-settings:keysList" ariaLabelKey="realm-settings:keysList"
searchPlaceholderKey="realm-settings:searchKey" searchPlaceholderKey="realm-settings:searchKey"

View file

@ -0,0 +1,247 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import {
Button,
ButtonVariant,
DataList,
DataListCell,
DataListControl,
DataListDragButton,
DataListItem,
DataListItemCells,
DataListItemRow,
Dropdown,
DropdownToggle,
InputGroup,
PageSection,
TextInput,
Toolbar,
ToolbarGroup,
ToolbarItem,
} from "@patternfly/react-core";
import type { KeyMetadataRepresentation } from "keycloak-admin/lib/defs/keyMetadataRepresentation";
import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation";
import "./RealmSettingsSection.css";
import type ComponentTypeRepresentation from "keycloak-admin/lib/defs/componentTypeRepresentation";
import { SearchIcon } from "@patternfly/react-icons";
type ComponentData = KeyMetadataRepresentation & {
providerDescription?: string;
name?: string;
};
type KeysTabInnerProps = {
components: ComponentData[];
realmComponents: ComponentRepresentation[];
keyProviderComponentTypes: ComponentTypeRepresentation[];
};
export const KeysTabInner = ({ components }: KeysTabInnerProps) => {
const { t } = useTranslation("roles");
const [id, setId] = useState("");
const [searchVal, setSearchVal] = useState("");
const [filteredComponents, setFilteredComponents] = useState<ComponentData[]>(
[]
);
const itemIds = components.map((item, idx) => "data" + idx);
const [itemOrder, setItemOrder] = useState<string[]>([]);
const [liveText, setLiveText] = useState("");
useEffect(() => {
setItemOrder(["data", ...itemIds]);
}, [components, searchVal]);
const onDragStart = (id: string) => {
setLiveText(t("onDragStart", { id }));
setId(id);
};
const onDragMove = () => {
setLiveText(t("onDragMove", { id }));
};
const onDragCancel = () => {
setLiveText(t("onDragCancel"));
};
const onDragFinish = (itemOrder: string[]) => {
setItemOrder(["data", ...itemOrder.filter((i) => i !== "data")]);
setLiveText(t("onDragCancel"));
};
const onSearch = () => {
if (searchVal !== "") {
setSearchVal(searchVal);
const x = components.filter((v) => {
return v.name?.includes(searchVal) || v.providerId?.includes(searchVal);
});
setFilteredComponents(x);
} else {
setSearchVal("");
setFilteredComponents(components);
}
};
const handleKeyDown = (e: any) => {
if (e.key === "Enter") {
onSearch();
}
};
const handleInputChange = (value: string) => {
setSearchVal(value);
};
return (
<>
<PageSection variant="light" padding={{ default: "noPadding" }}>
<Toolbar>
<>
<ToolbarGroup className="providers-toolbar">
<ToolbarItem>
<InputGroup>
<TextInput
name={"inputGroupName"}
id={"inputGroupName"}
type="search"
aria-label={t("common:search")}
placeholder={t("common:search")}
onChange={handleInputChange}
onKeyDown={handleKeyDown}
/>
<Button
variant={ButtonVariant.control}
aria-label={t("common:search")}
>
<SearchIcon />
</Button>
</InputGroup>
</ToolbarItem>
<ToolbarItem>
<Dropdown
data-testid="addProviderDropdown"
className="add-provider-dropdown"
onSelect={() => {}}
toggle={
<DropdownToggle isPrimary>
{t("realm-settings:addProvider")}
</DropdownToggle>
}
/>
</ToolbarItem>
</ToolbarGroup>
</>
</Toolbar>
<DataList
aria-label={t("groups")}
onDragFinish={onDragFinish}
onDragStart={onDragStart}
onDragMove={onDragMove}
onDragCancel={onDragCancel}
itemOrder={itemOrder}
isCompact
>
<DataListItem aria-labelledby={"aria"} id="data" key="data">
<DataListItemRow className="test" data-testid={"data-list-row"}>
<DataListDragButton
className="header-drag-button"
aria-label="Reorder"
aria-labelledby="simple-item"
aria-describedby="Press space or enter to begin dragging, and use the arrow keys to navigate up or down. Press enter to confirm the drag, or any other key to cancel the drag operation."
aria-pressed="false"
isDisabled
/>
<DataListItemCells
className="test2"
dataListCells={[
<DataListCell className="name" key={"1"}>
<>{t("realm-settings:name")}</>
</DataListCell>,
<DataListCell className="provider" key={"2"}>
<>{t("realm-settings:provider")}</>
</DataListCell>,
<DataListCell className="provider-description" key={"3"}>
<>{t("realm-settings:providerDescription")}</>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
{(filteredComponents.length === 0
? components
: filteredComponents
).map((component, idx) => (
<DataListItem
draggable
aria-labelledby={"aria"}
key={`data${idx}`}
id={`data${idx}`}
>
<DataListItemRow data-testid={"data-list-row"}>
<DataListControl>
<DataListDragButton
className="row-drag-button"
aria-label="Reorder"
aria-labelledby="simple-item2"
aria-describedby="Press space or enter to begin dragging, and use the arrow keys to navigate up or down. Press enter to confirm the drag, or any other key to cancel the drag operation."
aria-pressed="false"
/>
</DataListControl>
<DataListItemCells
dataListCells={[
<DataListCell key={"4"}>
<>
<Button variant="link">{component.providerId}</Button>
</>
</DataListCell>,
<DataListCell key={"5"}>
<>{component.name}</>
</DataListCell>,
<DataListCell key={"6"}>
<>{component.providerDescription}</>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
))}
</DataList>
<div className="pf-screen-reader" aria-live="assertive">
{liveText}
</div>
</PageSection>
</>
);
};
type KeysProps = {
components: ComponentRepresentation[];
realmComponents: ComponentRepresentation[];
keyProviderComponentTypes: ComponentTypeRepresentation[];
};
export const KeysProviderTab = ({
components,
keyProviderComponentTypes,
...props
}: KeysProps) => {
return (
<KeysTabInner
components={components?.map((component) => {
const provider = keyProviderComponentTypes.find(
(componentType: ComponentTypeRepresentation) =>
component.providerId === componentType.id
);
return { ...component, providerDescription: provider?.helpText };
})}
keyProviderComponentTypes={keyProviderComponentTypes}
{...props}
/>
);
};

View file

@ -1,185 +0,0 @@
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 type { 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 type 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 (
<>
<Button
onClick={() => {
togglePublicKeyDialog();
setPublicKey(publicKey!);
}}
variant="secondary"
id="kc-public-key"
>
{t("realm-settings:publicKeys").slice(0, -1)}
</Button>
</>
);
}
if (provider === "rsa-generated" || provider === "fallback-RS256") {
return (
<>
<Button
onClick={() => {
togglePublicKeyDialog();
setPublicKey(publicKey!);
}}
variant="secondary"
id="kc-rsa-public-key"
>
{t("realm-settings:publicKeys").slice(0, -1)}
</Button>
<Button
onClick={() => {
toggleCertificateDialog();
setCertificate(certificate!);
}}
variant="secondary"
id="kc-certificate"
>
{t("realm-settings:certificate")}
</Button>
</>
);
}
};
return (
<>
<PageSection variant="light" padding={{ default: "noPadding" }}>
<PublicKeyDialog />
<CertificateDialog />
<KeycloakDataTable
key={key}
loader={loader}
ariaLabelKey="realm-settings:keysList"
searchPlaceholderKey="realm-settings:searchKey"
canSelectAll
columns={[
{
name: "algorithm",
displayKey: "realm-settings:algorithm",
cellFormatters: [emptyFormatter()],
transforms: [cellWidth(15)],
},
{
name: "type",
displayKey: "realm-settings:type",
cellFormatters: [emptyFormatter()],
transforms: [cellWidth(10)],
},
{
name: "kid",
displayKey: "realm-settings:kid",
cellFormatters: [emptyFormatter()],
},
{
name: "provider",
displayKey: "realm-settings:provider",
cellRenderer: ProviderRenderer,
cellFormatters: [emptyFormatter()],
},
{
name: "publicKeys",
displayKey: "realm-settings:publicKeys",
cellRenderer: ButtonRenderer,
cellFormatters: [],
},
]}
emptyState={
<ListEmptyState
hasIcon={true}
message={t("noRoles")}
instructions={t("noRolesInstructions")}
primaryActionText={t("createRole")}
onPrimaryAction={goToCreate}
/>
}
/>
</PageSection>
</>
);
};
type KeysProps = {
keys: KeyMetadataRepresentation[];
realmComponents: ComponentRepresentation[];
};
export const KeysTab = ({ keys, realmComponents, ...props }: KeysProps) => {
return (
<KeysTabInner
keys={keys?.map((key) => {
const provider = realmComponents.find(
(component: ComponentRepresentation) =>
component.id === key.providerId
);
return { ...key, provider: provider?.providerId };
})}
{...props}
/>
);
};

View file

@ -16,6 +16,39 @@ div.pf-c-card__body.kc-form-panel__body {
padding-left: 0px; padding-left: 0px;
padding-bottom: var(--pf-global--spacer--2xl); padding-bottom: var(--pf-global--spacer--2xl);
} }
button#kc-certificate.pf-c-button.pf-m-secondary { button#kc-certificate.pf-c-button.pf-m-secondary {
margin-left: var(--pf-global--spacer--md); margin-left: var(--pf-global--spacer--md);
} }
.pf-c-data-list__item-row.test {
font-weight: bold;
}
.pf-c-data-list__item-content.test2 {
margin-left: var(--pf-global--spacer--xl);
}
button.pf-c-data-list__item-draggable-button.pf-m-disabled.header-drag-button {
display: none;
}
button.pf-c-data-list__item-draggable-button.row-drag-button {
padding-top: var(--pf-global--spacer--md);
}
.pf-c-data-list__item-control {
margin-right: 0px;
}
button.pf-c-button.pf-m-link.add-provider {
padding: 0px;
}
.pf-c-toolbar__group.providers-toolbar {
padding-left: var(--pf-c-toolbar__content--PaddingLeft);
}
.button-wrapper {
white-space: nowrap;
}

View file

@ -29,6 +29,8 @@ import { RealmSettingsEmailTab } from "./EmailTab";
import { KeysListTab } from "./KeysListTab"; import { KeysListTab } from "./KeysListTab";
import type { KeyMetadataRepresentation } from "keycloak-admin/lib/defs/keyMetadataRepresentation"; import type { KeyMetadataRepresentation } from "keycloak-admin/lib/defs/keyMetadataRepresentation";
import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation"; import type ComponentRepresentation from "keycloak-admin/lib/defs/componentRepresentation";
import { KeysProviderTab } from "./KeysProvidersTab";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
type RealmSettingsHeaderProps = { type RealmSettingsHeaderProps = {
onChange: (value: boolean) => void; onChange: (value: boolean) => void;
@ -134,6 +136,10 @@ export const RealmSettingsSection = () => {
ComponentRepresentation[] ComponentRepresentation[]
>([]); >([]);
const kpComponentTypes = useServerInfo().componentTypes![
"org.keycloak.keys.KeyProvider"
];
useFetch( useFetch(
() => adminClient.realms.findOne({ realm: realmName }), () => adminClient.realms.findOne({ realm: realmName }),
(realm) => { (realm) => {
@ -243,6 +249,17 @@ export const RealmSettingsSection = () => {
> >
<KeysListTab keys={keys} realmComponents={realmComponents} /> <KeysListTab keys={keys} realmComponents={realmComponents} />
</Tab> </Tab>
<Tab
id="evaluate"
eventKey={1}
title={<TabTitleText>{t("providers")}</TabTitleText>}
>
<KeysProviderTab
components={realmComponents}
realmComponents={realmComponents}
keyProviderComponentTypes={kpComponentTypes}
/>
</Tab>
</Tabs> </Tabs>
</Tab> </Tab>
</KeycloakTabs> </KeycloakTabs>

View file

@ -36,8 +36,11 @@
"providers": "Providers", "providers": "Providers",
"algorithm": "Algorithm", "algorithm": "Algorithm",
"type": "Type", "type": "Type",
"name": "Name",
"kid": "Kid", "kid": "Kid",
"provider": "Provider", "provider": "Provider",
"providerDescription": "Provider description",
"addProvider": "Add provider",
"publicKeys": "Public keys", "publicKeys": "Public keys",
"certificate": "Certificate", "certificate": "Certificate",
"userRegistration": "User registration", "userRegistration": "User registration",
@ -102,5 +105,9 @@
"partial-import": { "partial-import": {
"partialImportHeaderText": "Partial import allows you to import users, clients, and resources from a previously exported json file.", "partialImportHeaderText": "Partial import allows you to import users, clients, and resources from a previously exported json file.",
"import": "Import" "import": "Import"
} },
"onDragStart": "Dragging started for item {{id}}",
"onDragMove": "Dragging item {{id}}",
"onDragCancel": "Dragging cancelled. List is unchanged.",
"onDragFinish": "Dragging finished {{list}}"
} }