added the missing example code blocks (#2103)
This commit is contained in:
parent
6a4e490928
commit
2d55809caf
6 changed files with 234 additions and 35 deletions
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
|
|
90
src/clients/scopes/GeneratedCodeTab.tsx
Normal file
90
src/clients/scopes/GeneratedCodeTab.tsx
Normal 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>
|
||||
);
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -30,5 +30,10 @@ export default function useSetTimeout() {
|
|||
scheduledTimers.delete(timer);
|
||||
callback();
|
||||
}
|
||||
|
||||
return function cancelTimeout() {
|
||||
clearTimeout(timer);
|
||||
scheduledTimers.delete(timer);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue