added the missing example code blocks (#2103)

This commit is contained in:
Erik Jan de Wit 2022-02-23 15:46:26 +01:00 committed by GitHub
parent 6a4e490928
commit 2d55809caf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 234 additions and 35 deletions

View file

@ -92,7 +92,12 @@ export default {
"Contains all default client scopes and selected optional scopes. All protocol mappers and role scope mappings of all those client scopes will be used when generating access token issued for your client",
effectiveRoleScopeMappings:
"Selected Optional Client Scopes, which will be used when issuing access token for this client. You can see above what value of OAuth Scope Parameter needs to be used when you want to have these optional client scopes applied when the initial OpenID Connect Authentication request will be sent from your client adapter",
generatedAccessToken: "Example access token",
generatedAccessToken:
"See the example access token, which will be generated and sent to the client when selected user is authenticated. You can see claims and roles that the token will contain based on the effective protocol mappers and role scope mappings and also based on the claims/roles assigned to user himself",
generatedIdToken:
"See the example ID Token, which will be generated and sent to the client when selected user is authenticated. You can see claims and roles that the token will contain based on the effective protocol mappers and role scope mappings and also based on the claims/roles assigned to user himself",
generatedUserInfo:
"See the example User Info, which will be provided by the User Info Endpoint",
scopeParameter:
"You can copy/paste this value of scope parameter and use it in initial OpenID Connect Authentication Request sent from this client adapter. Default client scopes and selected optional client scopes will be used when generating token issued for this client",
user: "Optionally select user, for whom the example access token will be generated. If you do not select a user, example access token will not be generated during evaluation",

View file

@ -273,12 +273,20 @@ export default {
effectiveProtocolMappers: "Effective protocol mappers",
effectiveRoleScopeMappings: "Effective role scope mappings",
generatedAccessToken: "Generated access token",
generatedIdToken: "Generated ID token",
generatedIdTokenNo: "No generated id token",
generatedIdTokenIsDisabled:
"Generated id token is disabled when no user is selected",
generatedUserInfo: "Generated user info",
generatedUserInfoNo: "No generated user info",
generatedUserInfoIsDisabled:
"Generated user info is disabled when no user is selected",
searchForProtocol: "Search protocol mapper",
parentClientScope: "Parent client scope",
searchForRole: "Search role",
origin: "Origin",
user: "User",
noGeneratedAccessToken: "No generated access token",
generatedAccessTokenNo: "No generated access token",
generatedAccessTokenIsDisabled:
"Generated access token is disabled when no user is selected",
clientList: "Clients",
@ -330,6 +338,9 @@ export default {
initialAccessTokenDetails: "Initial access token details",
copyInitialAccessToken:
"Please copy and paste the initial access token before closing as it can not be retrieved later.",
copySuccess: "Successfully copied to clipboard!",
clipboardCopyError: "Error copying to clipboard.",
copyToClipboard: "Copy to clipboard",
clientAuthentication: "Client authentication",
authentication: "Authentication",
authenticationFlow: "Authentication flow",

View file

@ -1,7 +1,5 @@
import {
ClipboardCopy,
EmptyState,
EmptyStateBody,
Form,
FormGroup,
Grid,
@ -17,9 +15,7 @@ import {
Tabs,
TabTitleText,
Text,
TextArea,
TextContent,
Title,
} from "@patternfly/react-core";
import { QuestionCircleIcon } from "@patternfly/react-icons";
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
@ -36,6 +32,7 @@ import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { prettyPrintJSON } from "../../util";
import { GeneratedCodeTab } from "./GeneratedCodeTab";
import "./evaluate.css";
@ -140,10 +137,14 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
ProtocolMapperRepresentation[]
>([]);
const [accessToken, setAccessToken] = useState("");
const [userInfo, setUserInfo] = useState("");
const [idToken, setIdToken] = useState("");
const tabContent1 = useRef(null);
const tabContent2 = useRef(null);
const tabContent3 = useRef(null);
const tabContent4 = useRef(null);
const tabContent5 = useRef(null);
useFetch(
() => adminClient.clients.listOptionalClientScopes({ id: clientId }),
@ -221,20 +222,32 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
);
useFetch(
() => {
async () => {
const scope = selected.join(" ");
if (user) {
return adminClient.clients.evaluateGenerateAccessToken({
if (!user) return [];
return await Promise.all([
adminClient.clients.evaluateGenerateAccessToken({
id: clientId,
userId: user.id!,
scope,
});
} else {
return Promise.resolve({});
}
}),
adminClient.clients.evaluateGenerateUserInfo({
id: clientId,
userId: user.id!,
scope,
}),
adminClient.clients.evaluateGenerateIdToken({
id: clientId,
userId: user.id!,
scope,
}),
]);
},
(accessToken) => {
([accessToken, userInfo, idToken]) => {
setAccessToken(prettyPrintJSON(accessToken));
setUserInfo(prettyPrintJSON(userInfo));
setIdToken(prettyPrintJSON(idToken));
},
[user, selected]
);
@ -350,25 +363,43 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
<EffectiveRoles effectiveRoles={effectiveRoles} />
</TabContent>
<TabContent
aria-labelledby="pf-tab-0-generatedAccessToken"
aria-labelledby={t("generatedAccessToken")}
eventKey={2}
id="generatedAccessToken"
id="tab-generated-access-token"
ref={tabContent3}
hidden
>
{user && (
<TextArea rows={20} id="accessToken" value={accessToken} />
)}
{!user && (
<EmptyState variant="large">
<Title headingLevel="h4" size="lg">
{t("noGeneratedAccessToken")}
</Title>
<EmptyStateBody>
{t("generatedAccessTokenIsDisabled")}
</EmptyStateBody>
</EmptyState>
)}
<GeneratedCodeTab
text={accessToken}
user={user}
label="generatedAccessToken"
/>
</TabContent>
<TabContent
aria-labelledby={t("generatedIdToken")}
eventKey={3}
id="tab-generated-id-token"
ref={tabContent4}
hidden
>
<GeneratedCodeTab
text={idToken}
user={user}
label="generatedIdToken"
/>
</TabContent>
<TabContent
aria-labelledby={t("generatedUserInfo")}
eventKey={4}
id="tab-generated-user-info"
ref={tabContent5}
hidden
>
<GeneratedCodeTab
text={userInfo}
user={user}
label="generatedUserInfo"
/>
</TabContent>
</GridItem>
<GridItem span={4}>
@ -430,6 +461,40 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
}
tabContentRef={tabContent3}
/>
<Tab
id="generatedIdToken"
aria-controls="generatedIdToken"
eventKey={3}
title={
<TabTitleText>
{t("generatedIdToken")}{" "}
<HelpItem
fieldLabelId="clients:generatedIdToken"
helpText="clients-help:generatedIdToken"
noVerticalAlign={false}
unWrap
/>
</TabTitleText>
}
tabContentRef={tabContent4}
/>
<Tab
id="generatedUserInfo"
aria-controls="generatedUserInfo"
eventKey={4}
title={
<TabTitleText>
{t("generatedUserInfo")}{" "}
<HelpItem
fieldLabelId="clients:generatedUserInfo"
helpText="clients-help:generatedUserInfo"
noVerticalAlign={false}
unWrap
/>
</TabTitleText>
}
tabContentRef={tabContent5}
/>
</Tabs>
</GridItem>
</Grid>

View file

@ -0,0 +1,90 @@
import React, { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ClipboardCopyButton,
CodeBlock,
CodeBlockAction,
EmptyState,
EmptyStateBody,
TextArea,
Title,
} from "@patternfly/react-core";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import useSetTimeout from "../../utils/useSetTimeout";
type GeneratedCodeTabProps = {
user?: UserRepresentation;
text: string;
label: string;
};
enum CopyState {
Ready,
Copied,
Error,
}
export const GeneratedCodeTab = ({
text,
user,
label,
}: GeneratedCodeTabProps) => {
const { t } = useTranslation("clients");
const setTimeout = useSetTimeout();
const [copy, setCopy] = useState(CopyState.Ready);
const copyMessage = useMemo(() => {
switch (copy) {
case CopyState.Ready:
return t("copyToClipboard");
case CopyState.Copied:
return t("copySuccess");
case CopyState.Error:
return t("clipboardCopyError");
}
}, [copy]);
useEffect(() => {
if (copy !== CopyState.Ready) {
return setTimeout(() => setCopy(CopyState.Ready), 1000);
}
}, [copy]);
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text);
setCopy(CopyState.Copied);
} catch (error) {
setCopy(CopyState.Error);
}
};
return user ? (
<CodeBlock
id={label}
actions={
<CodeBlockAction>
<ClipboardCopyButton
id={`copy-button-${label}`}
textId={label}
aria-label={t("copyToClipboard")}
onClick={() => copyToClipboard(text)}
exitDelay={600}
variant="plain"
>
{copyMessage}
</ClipboardCopyButton>
</CodeBlockAction>
}
>
<TextArea id={`text-area-${label}`} rows={20} value={text} />
</CodeBlock>
) : (
<EmptyState variant="large">
<Title headingLevel="h4" size="lg">
{t(`${label}No`)}
</Title>
<EmptyStateBody>{t(`${label}IsDisabled`)}</EmptyStateBody>
</EmptyState>
);
};

View file

@ -45,17 +45,40 @@ describe("useSetTimeout", () => {
it("clears a timeout if the component unmounts", () => {
const { result, unmount } = renderHook(() => useSetTimeout());
const timerId = 42;
const setTimeoutSpy = jest
.spyOn(global, "setTimeout")
.mockReturnValueOnce(timerId as unknown as NodeJS.Timeout);
const setTimeoutSpy = jest.spyOn(global, "setTimeout");
const clearTimeoutSpy = jest.spyOn(global, "clearTimeout");
const callback = jest.fn();
result.current(jest.fn(), 1000);
result.current(callback, 1000);
// Timeout should be cleared after unmounting.
unmount();
expect(clearTimeoutSpy).toBeCalled();
expect(clearTimeoutSpy).toBeCalledWith(timerId);
// And the callback should no longer be called.
jest.runOnlyPendingTimers();
expect(callback).not.toBeCalled();
setTimeoutSpy.mockRestore();
clearTimeoutSpy.mockRestore();
});
it("clears a timeout when cancelled", () => {
const { result } = renderHook(() => useSetTimeout());
const setTimeoutSpy = jest.spyOn(global, "setTimeout");
const clearTimeoutSpy = jest.spyOn(global, "clearTimeout");
const callback = jest.fn();
const cancel = result.current(callback, 1000);
// Timeout should be cleared when cancelling.
cancel();
expect(clearTimeoutSpy).toBeCalled();
// And the callback should no longer be called.
jest.runOnlyPendingTimers();
expect(callback).not.toBeCalled();
setTimeoutSpy.mockRestore();
clearTimeoutSpy.mockRestore();
});
});

View file

@ -30,5 +30,10 @@ export default function useSetTimeout() {
scheduledTimers.delete(timer);
callback();
}
return function cancelTimeout() {
clearTimeout(timer);
scheduledTimers.delete(timer);
};
};
}