Advanced tab (#373)
* initial version of the advanced tab * added registered nodes * added fine grain open id connect configuration * added open id connect compatibility section * added advanced section * added backend * fixed type * renamed 'advanced' to advancedtab to prevent strange add of '/index.js' by snowpack * fixed storybook stories * change '_' to '-' because '_' is also used * fix spacing buttons * stop passing the form * cypress test for advanced tab * more tests * saml section * changed to use NumberInput * added authetnication flow override * fixed merge error * updated text and added link to settings tab * fixed test * added filter on flows and better reset * added now mandetory error handler * added sorting * Revert "changed to use NumberInput" This reverts commit 7829f2656ae8fc8ed4a4a6b1c4b1961241a09d8e. * allow users to put empty string as value * already on detail page after save * fixed merge error
This commit is contained in:
parent
da2fa32a69
commit
bfa0c6e1ea
46 changed files with 2112 additions and 351 deletions
|
@ -4,6 +4,8 @@ import ListingPage from "../support/pages/admin_console/ListingPage";
|
|||
import SidebarPage from "../support/pages/admin_console/SidebarPage";
|
||||
import CreateClientPage from "../support/pages/admin_console/manage/clients/CreateClientPage";
|
||||
import ModalUtils from "../support/util/ModalUtils";
|
||||
import AdvancedTab from "../support/pages/admin_console/manage/clients/AdvancedTab";
|
||||
import AdminClient from "../support/util/AdminClient";
|
||||
|
||||
let itemId = "client_crud";
|
||||
const loginPage = new LoginPage();
|
||||
|
@ -77,4 +79,65 @@ describe("Clients test", function () {
|
|||
listingPage.itemExist(itemId, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Advanced tab test", () => {
|
||||
const advancedTab = new AdvancedTab();
|
||||
let client: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.visit("");
|
||||
loginPage.logIn();
|
||||
sidebarPage.goToClients();
|
||||
|
||||
client = "client_" + (Math.random() + 1).toString(36).substring(7);
|
||||
|
||||
listingPage.goToCreateItem();
|
||||
|
||||
createClientPage
|
||||
.selectClientType("openid-connect")
|
||||
.fillClientData(client)
|
||||
.continue()
|
||||
.continue();
|
||||
|
||||
advancedTab.goToTab();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
new AdminClient().deleteClient(client);
|
||||
});
|
||||
|
||||
it("Revocation", () => {
|
||||
advancedTab.checkNone();
|
||||
|
||||
advancedTab.clickSetToNow().checkSetToNow();
|
||||
advancedTab.clickClear().checkNone();
|
||||
|
||||
advancedTab.clickPush();
|
||||
masthead.checkNotificationMessage(
|
||||
"No push sent. No admin URI configured or no registered cluster nodes available"
|
||||
);
|
||||
});
|
||||
|
||||
it("Clustering", () => {
|
||||
advancedTab.expandClusterNode().checkTestClusterAvailability(false);
|
||||
|
||||
advancedTab
|
||||
.clickRegisterNodeManually()
|
||||
.fillHost("localhost")
|
||||
.clickSaveHost();
|
||||
advancedTab.checkTestClusterAvailability(true);
|
||||
});
|
||||
|
||||
it("Fine grain OpenID connect configuration", () => {
|
||||
const algorithm = "ES384";
|
||||
advancedTab
|
||||
.selectAccessTokenSignatureAlgorithm(algorithm)
|
||||
.clickSaveFineGrain();
|
||||
|
||||
advancedTab
|
||||
.selectAccessTokenSignatureAlgorithm("HS384")
|
||||
.clickReloadFineGrain();
|
||||
advancedTab.checkAccessTokenSignatureAlgorithm(algorithm);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import moment from "moment";
|
||||
|
||||
export default class AdvancedTab {
|
||||
private setToNow = "#setToNow";
|
||||
private clear = "#clear";
|
||||
private push = "#push";
|
||||
private notBefore = "#kc-not-before";
|
||||
|
||||
private clusterNodesExpand =
|
||||
".pf-c-expandable-section .pf-c-expandable-section__toggle";
|
||||
private testClusterAvailability = "#testClusterAvailability";
|
||||
private registerNodeManually = "#registerNodeManually";
|
||||
private nodeHost = "#nodeHost";
|
||||
private addNodeConfirm = "#add-node-confirm";
|
||||
|
||||
private accessTokenSignatureAlgorithm = "#accessTokenSignatureAlgorithm";
|
||||
private fineGrainSave = "#fineGrainSave";
|
||||
private fineGrainReload = "#fineGrainReload";
|
||||
|
||||
private advancedTab = "#pf-tab-advanced-advanced";
|
||||
|
||||
goToTab() {
|
||||
cy.get(this.advancedTab).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
clickSetToNow() {
|
||||
cy.get(this.setToNow).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
clickClear() {
|
||||
cy.get(this.clear).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
clickPush() {
|
||||
cy.get(this.push).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
checkNone() {
|
||||
cy.get(this.notBefore).should("have.value", "None");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
checkSetToNow() {
|
||||
cy.get(this.notBefore).should("have.value", moment().format("LLL"));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
expandClusterNode() {
|
||||
cy.get(this.clusterNodesExpand).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
checkTestClusterAvailability(active: boolean) {
|
||||
cy.get(this.testClusterAvailability).should(
|
||||
(active ? "not." : "") + "have.class",
|
||||
"pf-m-disabled"
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
clickRegisterNodeManually() {
|
||||
cy.get(this.registerNodeManually).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
fillHost(host: string) {
|
||||
cy.get(this.nodeHost).type(host);
|
||||
return this;
|
||||
}
|
||||
|
||||
clickSaveHost() {
|
||||
cy.get(this.addNodeConfirm).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
selectAccessTokenSignatureAlgorithm(algorithm: string) {
|
||||
cy.get(this.accessTokenSignatureAlgorithm).click();
|
||||
cy.get(this.accessTokenSignatureAlgorithm + " + ul")
|
||||
.contains(algorithm)
|
||||
.click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
checkAccessTokenSignatureAlgorithm(algorithm: string) {
|
||||
cy.get(this.accessTokenSignatureAlgorithm).should("have.text", algorithm);
|
||||
return this;
|
||||
}
|
||||
|
||||
clickSaveFineGrain() {
|
||||
cy.get(this.fineGrainSave).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
clickReloadFineGrain() {
|
||||
cy.get(this.fineGrainReload).click();
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -22,4 +22,12 @@ export default class AdminClient {
|
|||
await this.login();
|
||||
await this.client.realms.del({ realm });
|
||||
}
|
||||
|
||||
async deleteClient(clientName: string) {
|
||||
await this.login();
|
||||
const client = (
|
||||
await this.client.clients.find({ clientId: clientName })
|
||||
)[0];
|
||||
await this.client.clients.del({ id: client.id! });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
|
@ -24,7 +24,7 @@
|
|||
"@patternfly/react-table": "4.23.0",
|
||||
"file-saver": "^2.0.2",
|
||||
"i18next": "^19.6.2",
|
||||
"keycloak-admin": "1.14.6",
|
||||
"keycloak-admin": "1.14.7",
|
||||
"lodash": "^4.17.20",
|
||||
"moment": "^2.29.1",
|
||||
"react": "^16.8.5",
|
||||
|
|
|
@ -69,13 +69,12 @@ export const ClientScopesSection = () => {
|
|||
columns={[
|
||||
{
|
||||
name: "name",
|
||||
displayKey: t("common:name"),
|
||||
cellRenderer: ClientScopeDetailLink,
|
||||
},
|
||||
{ name: "description", displayKey: t("common:description") },
|
||||
{ name: "description" },
|
||||
{
|
||||
name: "protocol",
|
||||
displayKey: t("protocol"),
|
||||
displayKey: "client-scopes:protocol",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
|
|
@ -299,7 +299,7 @@ export const RoleMappingForm = () => {
|
|||
ref={register()}
|
||||
type="text"
|
||||
id="newRoleName"
|
||||
name="config.new_role_name"
|
||||
name="config.new-role-name"
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
|
|
|
@ -211,7 +211,7 @@ export const MappingDetails = () => {
|
|||
ref={register()}
|
||||
type="text"
|
||||
id="prefix"
|
||||
name="config.usermodel_realmRoleMapping_rolePrefix"
|
||||
name="config.usermodel-realmRoleMapping-rolePrefix"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
|
@ -255,7 +255,7 @@ export const MappingDetails = () => {
|
|||
ref={register()}
|
||||
type="text"
|
||||
id="claimName"
|
||||
name="config.claim_name"
|
||||
name="config.claim-name"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
|
@ -270,7 +270,7 @@ export const MappingDetails = () => {
|
|||
fieldId="claimJsonType"
|
||||
>
|
||||
<Controller
|
||||
name="config.jsonType_label"
|
||||
name="config.jsonType-label"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
|
@ -308,7 +308,7 @@ export const MappingDetails = () => {
|
|||
<Flex>
|
||||
<FlexItem>
|
||||
<Controller
|
||||
name="config.id_token_claim"
|
||||
name="config.id-token-claim"
|
||||
defaultValue="false"
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
|
@ -323,7 +323,7 @@ export const MappingDetails = () => {
|
|||
</FlexItem>
|
||||
<FlexItem>
|
||||
<Controller
|
||||
name="config.access_token_claim"
|
||||
name="config.access-token-claim"
|
||||
defaultValue="false"
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
|
@ -338,7 +338,7 @@ export const MappingDetails = () => {
|
|||
</FlexItem>
|
||||
<FlexItem>
|
||||
<Controller
|
||||
name="config.userinfo_token_claim"
|
||||
name="config.userinfo-token-claim"
|
||||
defaultValue="false"
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
|
|
|
@ -218,7 +218,7 @@ export const ClientScopeForm = () => {
|
|||
fieldId="kc-display.on.consent.screen"
|
||||
>
|
||||
<Controller
|
||||
name="attributes.display_on_consent_screen"
|
||||
name="attributes.display-on-consent-screen"
|
||||
control={control}
|
||||
defaultValue="false"
|
||||
render={({ onChange, value }) => (
|
||||
|
@ -247,7 +247,7 @@ export const ClientScopeForm = () => {
|
|||
ref={register}
|
||||
type="text"
|
||||
id="kc-consent-screen-text"
|
||||
name="attributes.consent_screen_text"
|
||||
name="attributes.consent-screen-text"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
|
@ -263,7 +263,7 @@ export const ClientScopeForm = () => {
|
|||
fieldId="includeInTokenScope"
|
||||
>
|
||||
<Controller
|
||||
name="attributes.include_in_token_scope"
|
||||
name="attributes.include-in-token-scope"
|
||||
control={control}
|
||||
defaultValue="false"
|
||||
render={({ onChange, value }) => (
|
||||
|
@ -298,7 +298,7 @@ export const ClientScopeForm = () => {
|
|||
ref={register({ pattern: /^([0-9]*)$/ })}
|
||||
type="text"
|
||||
id="kc-gui-order"
|
||||
name="attributes.gui_order"
|
||||
name="attributes.gui-order"
|
||||
validated={
|
||||
errors.attributes && errors.attributes["gui_order"]
|
||||
? ValidatedOptions.error
|
||||
|
|
449
src/clients/AdvancedTab.tsx
Normal file
449
src/clients/AdvancedTab.tsx
Normal file
|
@ -0,0 +1,449 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import moment from "moment";
|
||||
import {
|
||||
ActionGroup,
|
||||
AlertVariant,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Card,
|
||||
CardBody,
|
||||
ExpandableSection,
|
||||
FormGroup,
|
||||
Split,
|
||||
SplitItem,
|
||||
Text,
|
||||
TextInput,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import GlobalRequestResult from "keycloak-admin/lib/defs/globalRequestResult";
|
||||
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||
import { convertToFormValues, toUpperCase } from "../util";
|
||||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
import { ScrollForm } from "../components/scroll-form/ScrollForm";
|
||||
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
import { FineGrainOpenIdConnect } from "./advanced/FineGrainOpenIdConnect";
|
||||
import { OpenIdConnectCompatibilityModes } from "./advanced/OpenIdConnectCompatibilityModes";
|
||||
import { AdvancedSettings } from "./advanced/AdvancedSettings";
|
||||
import { TimeSelector } from "../components/time-selector/TimeSelector";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { AddHostDialog } from "./advanced/AddHostDialog";
|
||||
import { FineGrainSamlEndpointConfig } from "./advanced/FineGrainSamlEndpointConfig";
|
||||
import { AuthenticationOverrides } from "./advanced/AuthenticationOverrides";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
|
||||
type AdvancedProps = {
|
||||
save: () => void;
|
||||
client: ClientRepresentation;
|
||||
};
|
||||
|
||||
export const AdvancedTab = ({
|
||||
save,
|
||||
client: {
|
||||
id,
|
||||
registeredNodes,
|
||||
attributes,
|
||||
protocol,
|
||||
authenticationFlowBindingOverrides,
|
||||
},
|
||||
}: AdvancedProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const { realm } = useRealm();
|
||||
const { addAlert } = useAlerts();
|
||||
const revocationFieldName = "notBefore";
|
||||
const openIdConnect = "openid-connect";
|
||||
|
||||
const { getValues, setValue, register, control, reset } = useFormContext();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [selectedNode, setSelectedNode] = useState("");
|
||||
const [addNodeOpen, setAddNodeOpen] = useState(false);
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
const [nodes, setNodes] = useState(registeredNodes || {});
|
||||
|
||||
const setNotBefore = (time: number) => {
|
||||
setValue(revocationFieldName, time);
|
||||
save();
|
||||
};
|
||||
|
||||
const parseResult = (result: GlobalRequestResult, prefixKey: string) => {
|
||||
const successCount = result.successRequests?.length || 0;
|
||||
const failedCount = result.failedRequests?.length || 0;
|
||||
|
||||
if (successCount === 0 && failedCount === 0) {
|
||||
addAlert(t("noAdminUrlSet"), AlertVariant.warning);
|
||||
} else if (failedCount > 0) {
|
||||
addAlert(
|
||||
t(prefixKey + "Success", { successNodes: result.successRequests }),
|
||||
AlertVariant.success
|
||||
);
|
||||
addAlert(
|
||||
t(prefixKey + "Fail", { failedNodes: result.failedRequests }),
|
||||
AlertVariant.danger
|
||||
);
|
||||
} else {
|
||||
addAlert(
|
||||
t(prefixKey + "Success", { successNodes: result.successRequests }),
|
||||
AlertVariant.success
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const resetFields = (names: string[]) => {
|
||||
const values: { [name: string]: string } = {};
|
||||
for (const name of names) {
|
||||
values[`attributes.${name}`] = attributes
|
||||
? attributes[name.replace(/-/g, ".")] || ""
|
||||
: "";
|
||||
}
|
||||
reset(values);
|
||||
};
|
||||
|
||||
const push = async () => {
|
||||
const result = ((await adminClient.clients.pushRevocation({
|
||||
id: id!,
|
||||
})) as unknown) as GlobalRequestResult;
|
||||
parseResult(result, "notBeforePush");
|
||||
};
|
||||
|
||||
const testCluster = async () => {
|
||||
const result = await adminClient.clients.testNodesAvailable({ id: id! });
|
||||
parseResult(result, "testCluster");
|
||||
};
|
||||
|
||||
const [toggleDeleteNodeConfirm, DeleteNodeConfirm] = useConfirmDialog({
|
||||
titleKey: "clients:deleteNode",
|
||||
messageKey: t("deleteNodeBody", {
|
||||
node: selectedNode,
|
||||
}),
|
||||
continueButtonLabel: "common:delete",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await adminClient.clients.deleteClusterNode({
|
||||
id: id!,
|
||||
node: selectedNode,
|
||||
});
|
||||
setNodes({
|
||||
...Object.keys(nodes).reduce((object: any, key) => {
|
||||
if (key !== selectedNode) {
|
||||
object[key] = nodes[key];
|
||||
}
|
||||
return object;
|
||||
}, {}),
|
||||
});
|
||||
refresh();
|
||||
addAlert(t("deleteNodeSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("deleteNodeFail", { error: error.response?.data?.error || error }),
|
||||
AlertVariant.danger
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
register(revocationFieldName);
|
||||
}, [register]);
|
||||
|
||||
const formatDate = () => {
|
||||
const date = getValues(revocationFieldName);
|
||||
if (date > 0) {
|
||||
return moment(date * 1000).format("LLL");
|
||||
} else {
|
||||
return t("none");
|
||||
}
|
||||
};
|
||||
|
||||
const sections = [
|
||||
t("revocation"),
|
||||
t("clustering"),
|
||||
protocol === openIdConnect
|
||||
? t("fineGrainOpenIdConnectConfiguration")
|
||||
: t("fineGrainSamlEndpointConfig"),
|
||||
t("advancedSettings"),
|
||||
t("authenticationOverrides"),
|
||||
];
|
||||
if (protocol === openIdConnect) {
|
||||
sections.splice(3, 0, t("openIdConnectCompatibilityModes"));
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollForm sections={sections}>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Text>
|
||||
<Trans i18nKey="clients-help:notBeforeIntro">
|
||||
In order to successfully push setup url on
|
||||
<Link to={`/${realm}/clients/${id}/settings`}>
|
||||
{t("settings")}
|
||||
</Link>
|
||||
tab
|
||||
</Trans>
|
||||
</Text>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<FormAccess role="manage-clients" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("notBefore")}
|
||||
fieldId="kc-not-before"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:notBefore"
|
||||
forLabel={t("notBefore")}
|
||||
forID="kc-not-before"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="kc-not-before"
|
||||
name="notBefore"
|
||||
isReadOnly
|
||||
value={formatDate()}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
id="setToNow"
|
||||
variant="tertiary"
|
||||
onClick={() => setNotBefore(moment.now() / 1000)}
|
||||
>
|
||||
{t("setToNow")}
|
||||
</Button>
|
||||
<Button
|
||||
id="clear"
|
||||
variant="tertiary"
|
||||
onClick={() => setNotBefore(0)}
|
||||
>
|
||||
{t("clear")}
|
||||
</Button>
|
||||
<Button id="push" variant="secondary" onClick={push}>
|
||||
{t("push")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<FormAccess role="manage-clients" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("nodeReRegistrationTimeout")}
|
||||
fieldId="kc-node-reregistration-timeout"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:nodeReRegistrationTimeout"
|
||||
forLabel={t("nodeReRegistrationTimeout")}
|
||||
forID="nodeReRegistrationTimeout"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Split hasGutter>
|
||||
<SplitItem>
|
||||
<Controller
|
||||
name="nodeReRegistrationTimeout"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<TimeSelector value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<Button variant={ButtonVariant.secondary} onClick={save}>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</FormGroup>
|
||||
</FormAccess>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<DeleteNodeConfirm />
|
||||
<AddHostDialog
|
||||
clientId={id!}
|
||||
isOpen={addNodeOpen}
|
||||
onAdded={(node) => {
|
||||
nodes[node] = moment.now() / 1000;
|
||||
refresh();
|
||||
}}
|
||||
onClose={() => setAddNodeOpen(false)}
|
||||
/>
|
||||
<ExpandableSection
|
||||
toggleText={t("registeredClusterNodes")}
|
||||
onToggle={() => setExpanded(!expanded)}
|
||||
isExpanded={expanded}
|
||||
>
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
ariaLabelKey="registeredClusterNodes"
|
||||
loader={() =>
|
||||
Promise.resolve(
|
||||
Object.entries(nodes || {}).map((entry) => {
|
||||
return { host: entry[0], registration: entry[1] };
|
||||
})
|
||||
)
|
||||
}
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
id="testClusterAvailability"
|
||||
onClick={testCluster}
|
||||
variant={ButtonVariant.secondary}
|
||||
isDisabled={Object.keys(nodes).length === 0}
|
||||
>
|
||||
{t("testClusterAvailability")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
id="registerNodeManually"
|
||||
onClick={() => setAddNodeOpen(true)}
|
||||
variant={ButtonVariant.tertiary}
|
||||
>
|
||||
{t("registerNodeManually")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
title: t("common:delete"),
|
||||
onRowClick: (node) => {
|
||||
setSelectedNode(node.host);
|
||||
toggleDeleteNodeConfirm();
|
||||
},
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
name: "host",
|
||||
displayKey: "clients:nodeHost",
|
||||
},
|
||||
{
|
||||
name: "registration",
|
||||
displayKey: "clients:lastRegistration",
|
||||
cellFormatters: [
|
||||
(value) =>
|
||||
value
|
||||
? moment(parseInt(value.toString()) * 1000).format(
|
||||
"LLL"
|
||||
)
|
||||
: "",
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ExpandableSection>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card>
|
||||
{protocol === openIdConnect && (
|
||||
<>
|
||||
<CardBody>
|
||||
<Text>
|
||||
{t("clients-help:fineGrainOpenIdConnectConfiguration")}
|
||||
</Text>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<FineGrainOpenIdConnect
|
||||
control={control}
|
||||
save={save}
|
||||
reset={() =>
|
||||
convertToFormValues(attributes, "attributes", setValue)
|
||||
}
|
||||
/>
|
||||
</CardBody>
|
||||
</>
|
||||
)}
|
||||
{protocol !== openIdConnect && (
|
||||
<>
|
||||
<CardBody>
|
||||
<Text>{t("clients-help:fineGrainSamlEndpointConfig")}</Text>
|
||||
</CardBody>
|
||||
|
||||
<CardBody>
|
||||
<FineGrainSamlEndpointConfig
|
||||
control={control}
|
||||
save={save}
|
||||
reset={() =>
|
||||
convertToFormValues(attributes, "attributes", setValue)
|
||||
}
|
||||
/>
|
||||
</CardBody>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
{protocol === openIdConnect && (
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Text>{t("clients-help:openIdConnectCompatibilityModes")}</Text>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<OpenIdConnectCompatibilityModes
|
||||
control={control}
|
||||
save={save}
|
||||
reset={() =>
|
||||
resetFields(["exclude-session-state-from-auth-response"])
|
||||
}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Text>
|
||||
{t("clients-help:advancedSettings" + toUpperCase(protocol!))}
|
||||
</Text>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<AdvancedSettings
|
||||
protocol={protocol}
|
||||
control={control}
|
||||
save={save}
|
||||
reset={() => {
|
||||
resetFields([
|
||||
"saml-assertion-lifespan",
|
||||
"access-token-lifespan",
|
||||
"tls-client-certificate-bound-access-tokens",
|
||||
"pkce-code-challenge-method",
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardBody>
|
||||
<Text>{t("clients-help:authenticationOverrides")}</Text>
|
||||
</CardBody>
|
||||
<CardBody>
|
||||
<AuthenticationOverrides
|
||||
protocol={protocol}
|
||||
control={control}
|
||||
save={save}
|
||||
reset={() => {
|
||||
setValue(
|
||||
"authenticationFlowBindingOverrides.browser",
|
||||
authenticationFlowBindingOverrides?.browser
|
||||
);
|
||||
setValue(
|
||||
"authenticationFlowBindingOverrides.direct_grant",
|
||||
authenticationFlowBindingOverrides?.direct_grant
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</ScrollForm>
|
||||
);
|
||||
};
|
|
@ -1,16 +1,14 @@
|
|||
import React from "react";
|
||||
import { FormGroup, TextInput, ValidatedOptions } from "@patternfly/react-core";
|
||||
import { UseFormMethods } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
import { ClientForm } from "./ClientDetails";
|
||||
|
||||
type ClientDescriptionProps = {
|
||||
form: UseFormMethods;
|
||||
};
|
||||
|
||||
export const ClientDescription = ({ form }: ClientDescriptionProps) => {
|
||||
export const ClientDescription = () => {
|
||||
const { t } = useTranslation("clients");
|
||||
const { register, errors } = form;
|
||||
const { register, errors } = useFormContext<ClientForm>();
|
||||
return (
|
||||
<FormAccess role="manage-clients" unWrap>
|
||||
<FormGroup
|
||||
|
|
|
@ -11,13 +11,14 @@ import {
|
|||
import { useParams } from "react-router-dom";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { Controller, FormProvider, useForm, useWatch } from "react-hook-form";
|
||||
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||
import _ from "lodash";
|
||||
|
||||
import { ClientSettings } from "./ClientSettings";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { useDownloadDialog } from "../components/download-dialog/DownloadDialog";
|
||||
import { DownloadDialog } from "../components/download-dialog/DownloadDialog";
|
||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||
import { useAdminClient, asyncStateFetch } from "../context/auth/AdminClient";
|
||||
import { Credentials } from "./credentials/Credentials";
|
||||
|
@ -28,6 +29,7 @@ import {
|
|||
} from "../util";
|
||||
import {
|
||||
convertToMultiline,
|
||||
MultiLine,
|
||||
toValue,
|
||||
} from "../components/multi-line-input/MultiLineInput";
|
||||
import { ClientScopes } from "./scopes/ClientScopes";
|
||||
|
@ -35,6 +37,7 @@ import { EvaluateScopes } from "./scopes/EvaluateScopes";
|
|||
import { RolesList } from "../realm-roles/RolesList";
|
||||
import { ServiceAccount } from "./service-account/ServiceAccount";
|
||||
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
|
||||
import { AdvancedTab } from "./AdvancedTab";
|
||||
|
||||
type ClientDetailHeaderProps = {
|
||||
onChange: (value: boolean) => void;
|
||||
|
@ -94,14 +97,24 @@ const ClientDetailHeader = ({
|
|||
);
|
||||
};
|
||||
|
||||
export type ClientForm = Omit<
|
||||
ClientRepresentation,
|
||||
"redirectUris" | "webOrigins"
|
||||
> & {
|
||||
redirectUris: MultiLine[];
|
||||
webOrigins: MultiLine[];
|
||||
};
|
||||
|
||||
export const ClientDetails = () => {
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
const { addAlert } = useAlerts();
|
||||
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false);
|
||||
const toggleDownloadDialog = () => setDownloadDialogOpen(!downloadDialogOpen);
|
||||
|
||||
const form = useForm();
|
||||
const form = useForm<ClientForm>();
|
||||
const publicClient = useWatch({
|
||||
control: form.control,
|
||||
name: "publicClient",
|
||||
|
@ -114,18 +127,7 @@ export const ClientDetails = () => {
|
|||
|
||||
const loader = async () => {
|
||||
const roles = await adminClient.clients.listRoles({ id });
|
||||
return roles.sort((r1, r2) => {
|
||||
const r1Name = r1.name?.toUpperCase();
|
||||
const r2Name = r2.name?.toUpperCase();
|
||||
if (r1Name! < r2Name!) {
|
||||
return -1;
|
||||
}
|
||||
if (r1Name! > r2Name!) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
return _.sortBy(roles, (role) => role.name?.toUpperCase());
|
||||
};
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
|
@ -143,13 +145,10 @@ export const ClientDetails = () => {
|
|||
},
|
||||
});
|
||||
|
||||
const [toggleDownloadDialog, DownloadDialog] = useDownloadDialog({
|
||||
id,
|
||||
protocol: form.getValues("protocol"),
|
||||
});
|
||||
|
||||
const setupForm = (client: ClientRepresentation) => {
|
||||
form.reset(client);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { redirectUris, webOrigins, ...formValues } = client;
|
||||
form.reset(formValues);
|
||||
Object.entries(client).map((entry) => {
|
||||
if (entry[0] === "redirectUris" || entry[0] === "webOrigins") {
|
||||
form.setValue(entry[0], convertToMultiline(entry[1]));
|
||||
|
@ -170,26 +169,27 @@ export const ClientDetails = () => {
|
|||
},
|
||||
handleError
|
||||
);
|
||||
}, []);
|
||||
}, [id]);
|
||||
|
||||
const save = async () => {
|
||||
if (await form.trigger()) {
|
||||
const redirectUris = toValue(form.getValues()["redirectUris"]);
|
||||
const webOrigins = toValue(form.getValues()["webOrigins"]);
|
||||
const attributes = form.getValues()["attributes"]
|
||||
? convertFormValuesToObject(form.getValues()["attributes"])
|
||||
: {};
|
||||
const attributes = convertFormValuesToObject(
|
||||
form.getValues()["attributes"]
|
||||
);
|
||||
|
||||
try {
|
||||
const client = {
|
||||
const newClient: ClientRepresentation = {
|
||||
...client,
|
||||
...form.getValues(),
|
||||
redirectUris,
|
||||
webOrigins,
|
||||
attributes,
|
||||
};
|
||||
await adminClient.clients.update({ id }, client);
|
||||
setupForm(client as ClientRepresentation);
|
||||
setClient(client);
|
||||
await adminClient.clients.update({ id }, newClient);
|
||||
setupForm(newClient);
|
||||
setClient(newClient);
|
||||
addAlert(t("clientSaveSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(`${t("clientSaveError")} '${error}'`, AlertVariant.danger);
|
||||
|
@ -207,7 +207,12 @@ export const ClientDetails = () => {
|
|||
return (
|
||||
<>
|
||||
<DeleteConfirm />
|
||||
<DownloadDialog />
|
||||
<DownloadDialog
|
||||
id={client.id!}
|
||||
protocol={client.protocol}
|
||||
open={downloadDialogOpen}
|
||||
toggleDialog={toggleDownloadDialog}
|
||||
/>
|
||||
<Controller
|
||||
name="enabled"
|
||||
control={form.control}
|
||||
|
@ -224,39 +229,46 @@ export const ClientDetails = () => {
|
|||
)}
|
||||
/>
|
||||
<PageSection variant="light">
|
||||
<FormProvider {...form}>
|
||||
<KeycloakTabs isBox>
|
||||
<Tab
|
||||
id="settings"
|
||||
eventKey="settings"
|
||||
title={<TabTitleText>{t("common:settings")}</TabTitleText>}
|
||||
>
|
||||
<ClientSettings form={form} save={save} />
|
||||
<ClientSettings save={save} />
|
||||
</Tab>
|
||||
{publicClient && (
|
||||
<Tab
|
||||
id="credentials"
|
||||
eventKey="credentials"
|
||||
title={<TabTitleText>{t("credentials")}</TabTitleText>}
|
||||
>
|
||||
<Credentials clientId={id} form={form} save={save} />
|
||||
<Credentials clientId={id} save={save} />
|
||||
</Tab>
|
||||
)}
|
||||
<Tab
|
||||
id="roles"
|
||||
eventKey="roles"
|
||||
title={<TabTitleText>{t("roles")}</TabTitleText>}
|
||||
>
|
||||
<RolesList loader={loader} paginated={false} />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="clientScopes"
|
||||
eventKey="clientScopes"
|
||||
title={<TabTitleText>{t("clientScopes")}</TabTitleText>}
|
||||
>
|
||||
<KeycloakTabs paramName="subtab" isSecondary>
|
||||
<Tab
|
||||
id="setup"
|
||||
eventKey="setup"
|
||||
title={<TabTitleText>{t("setup")}</TabTitleText>}
|
||||
>
|
||||
<ClientScopes clientId={id} protocol={client!.protocol!} />
|
||||
</Tab>
|
||||
<Tab
|
||||
id="evaluate"
|
||||
eventKey="evaluate"
|
||||
title={<TabTitleText>{t("evaluate")}</TabTitleText>}
|
||||
>
|
||||
|
@ -264,15 +276,24 @@ export const ClientDetails = () => {
|
|||
</Tab>
|
||||
</KeycloakTabs>
|
||||
</Tab>
|
||||
{client && client.serviceAccountsEnabled && (
|
||||
{client!.serviceAccountsEnabled && (
|
||||
<Tab
|
||||
id="serviceAccount"
|
||||
eventKey="serviceAccount"
|
||||
title={<TabTitleText>{t("serviceAccount")}</TabTitleText>}
|
||||
>
|
||||
<ServiceAccount clientId={id} />
|
||||
</Tab>
|
||||
)}
|
||||
<Tab
|
||||
id="advanced"
|
||||
eventKey="advanced"
|
||||
title={<TabTitleText>{t("advanced")}</TabTitleText>}
|
||||
>
|
||||
<AdvancedTab save={save} client={client} />
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
</FormProvider>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
ActionGroup,
|
||||
Button,
|
||||
} from "@patternfly/react-core";
|
||||
import { Controller, UseFormMethods } from "react-hook-form";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { ScrollForm } from "../components/scroll-form/ScrollForm";
|
||||
import { ClientDescription } from "./ClientDescription";
|
||||
|
@ -19,11 +19,11 @@ import { FormAccess } from "../components/form-access/FormAccess";
|
|||
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||
|
||||
type ClientSettingsProps = {
|
||||
form: UseFormMethods;
|
||||
save: () => void;
|
||||
};
|
||||
|
||||
export const ClientSettings = ({ form, save }: ClientSettingsProps) => {
|
||||
export const ClientSettings = ({ save }: ClientSettingsProps) => {
|
||||
const { register, control } = useFormContext();
|
||||
const { t } = useTranslation("clients");
|
||||
|
||||
return (
|
||||
|
@ -36,9 +36,9 @@ export const ClientSettings = ({ form, save }: ClientSettingsProps) => {
|
|||
t("loginSettings"),
|
||||
]}
|
||||
>
|
||||
<CapabilityConfig form={form} />
|
||||
<CapabilityConfig />
|
||||
<Form isHorizontal>
|
||||
<ClientDescription form={form} />
|
||||
<ClientDescription />
|
||||
</Form>
|
||||
<FormAccess isHorizontal role="manage-clients">
|
||||
<FormGroup label={t("rootUrl")} fieldId="kc-root-url">
|
||||
|
@ -46,18 +46,18 @@ export const ClientSettings = ({ form, save }: ClientSettingsProps) => {
|
|||
type="text"
|
||||
id="kc-root-url"
|
||||
name="rootUrl"
|
||||
ref={form.register}
|
||||
ref={register}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup label={t("validRedirectUri")} fieldId="kc-redirect">
|
||||
<MultiLineInput form={form} name="redirectUris" />
|
||||
<MultiLineInput name="redirectUris" />
|
||||
</FormGroup>
|
||||
<FormGroup label={t("homeURL")} fieldId="kc-home-url">
|
||||
<TextInput
|
||||
type="text"
|
||||
id="kc-home-url"
|
||||
name="baseUrl"
|
||||
ref={form.register}
|
||||
ref={register}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
|
@ -71,7 +71,7 @@ export const ClientSettings = ({ form, save }: ClientSettingsProps) => {
|
|||
/>
|
||||
}
|
||||
>
|
||||
<MultiLineInput form={form} name="webOrigins" />
|
||||
<MultiLineInput name="webOrigins" />
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("adminURL")}
|
||||
|
@ -88,7 +88,7 @@ export const ClientSettings = ({ form, save }: ClientSettingsProps) => {
|
|||
type="text"
|
||||
id="kc-admin-url"
|
||||
name="adminUrl"
|
||||
ref={form.register}
|
||||
ref={register}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormAccess>
|
||||
|
@ -101,7 +101,7 @@ export const ClientSettings = ({ form, save }: ClientSettingsProps) => {
|
|||
<Controller
|
||||
name="consentRequired"
|
||||
defaultValue={false}
|
||||
control={form.control}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="kc-consent"
|
||||
|
@ -119,16 +119,16 @@ export const ClientSettings = ({ form, save }: ClientSettingsProps) => {
|
|||
hasNoPaddingTop
|
||||
>
|
||||
<Controller
|
||||
name="attributes.display_on_consent_screen"
|
||||
name="attributes.display-on-consent-screen"
|
||||
defaultValue={false}
|
||||
control={form.control}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="kc-display-on-client"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value}
|
||||
onChange={onChange}
|
||||
isChecked={value === "true"}
|
||||
onChange={(value) => onChange("" + value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
@ -139,12 +139,12 @@ export const ClientSettings = ({ form, save }: ClientSettingsProps) => {
|
|||
>
|
||||
<TextArea
|
||||
id="kc-consent-screen-text"
|
||||
name="attributes.consent_screen_text"
|
||||
ref={form.register}
|
||||
name="attributes.consent-screen-text"
|
||||
ref={register}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button variant="primary" onClick={() => save()}>
|
||||
<Button variant="primary" onClick={save}>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button variant="link">{t("common:cancel")}</Button>
|
||||
|
|
|
@ -7,16 +7,14 @@ import {
|
|||
Grid,
|
||||
GridItem,
|
||||
} from "@patternfly/react-core";
|
||||
import { UseFormMethods, Controller } from "react-hook-form";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { ClientForm } from "../ClientDetails";
|
||||
|
||||
type CapabilityConfigProps = {
|
||||
form: UseFormMethods;
|
||||
};
|
||||
|
||||
export const CapabilityConfig = ({ form }: CapabilityConfigProps) => {
|
||||
export const CapabilityConfig = () => {
|
||||
const { t } = useTranslation("clients");
|
||||
const { control } = useFormContext<ClientForm>();
|
||||
return (
|
||||
<FormAccess isHorizontal role="manage-clients">
|
||||
<FormGroup
|
||||
|
@ -27,7 +25,7 @@ export const CapabilityConfig = ({ form }: CapabilityConfigProps) => {
|
|||
<Controller
|
||||
name="publicClient"
|
||||
defaultValue={false}
|
||||
control={form.control}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="kc-authentication"
|
||||
|
@ -48,7 +46,7 @@ export const CapabilityConfig = ({ form }: CapabilityConfigProps) => {
|
|||
<Controller
|
||||
name="authorizationServicesEnabled"
|
||||
defaultValue={false}
|
||||
control={form.control}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="kc-authorization"
|
||||
|
@ -71,7 +69,7 @@ export const CapabilityConfig = ({ form }: CapabilityConfigProps) => {
|
|||
<Controller
|
||||
name="standardFlowEnabled"
|
||||
defaultValue={false}
|
||||
control={form.control}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Checkbox
|
||||
label={t("standardFlow")}
|
||||
|
@ -87,7 +85,7 @@ export const CapabilityConfig = ({ form }: CapabilityConfigProps) => {
|
|||
<Controller
|
||||
name="directAccessGrantsEnabled"
|
||||
defaultValue={false}
|
||||
control={form.control}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Checkbox
|
||||
label={t("directAccess")}
|
||||
|
@ -103,7 +101,7 @@ export const CapabilityConfig = ({ form }: CapabilityConfigProps) => {
|
|||
<Controller
|
||||
name="implicitFlowEnabled"
|
||||
defaultValue={false}
|
||||
control={form.control}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Checkbox
|
||||
label={t("implicitFlow")}
|
||||
|
@ -119,7 +117,7 @@ export const CapabilityConfig = ({ form }: CapabilityConfigProps) => {
|
|||
<Controller
|
||||
name="serviceAccountsEnabled"
|
||||
defaultValue={false}
|
||||
control={form.control}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Checkbox
|
||||
label={t("serviceAccount")}
|
||||
|
|
|
@ -6,19 +6,15 @@ import {
|
|||
SelectOption,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, UseFormMethods } from "react-hook-form";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import { useLoginProviders } from "../../context/server-info/ServerInfoProvider";
|
||||
import { ClientDescription } from "../ClientDescription";
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
|
||||
type GeneralSettingsProps = {
|
||||
form: UseFormMethods;
|
||||
};
|
||||
|
||||
export const GeneralSettings = ({ form }: GeneralSettingsProps) => {
|
||||
export const GeneralSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const { errors, control } = form;
|
||||
const { errors, control } = useFormContext();
|
||||
|
||||
const providers = useLoginProviders();
|
||||
const [open, isOpen] = useState(false);
|
||||
|
@ -63,7 +59,7 @@ export const GeneralSettings = ({ form }: GeneralSettingsProps) => {
|
|||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ClientDescription form={form} />
|
||||
<ClientDescription />
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
Button,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
|
||||
import { GeneralSettings } from "./GeneralSettings";
|
||||
import { CapabilityConfig } from "./CapabilityConfig";
|
||||
|
@ -99,6 +99,7 @@ export const NewClientForm = () => {
|
|||
subKey="clients:clientsExplain"
|
||||
/>
|
||||
<PageSection variant="light">
|
||||
<FormProvider {...methods}>
|
||||
<Wizard
|
||||
onClose={() => history.push(`/${realm}/clients`)}
|
||||
navAriaLabel={`${title} steps`}
|
||||
|
@ -106,16 +107,17 @@ export const NewClientForm = () => {
|
|||
steps={[
|
||||
{
|
||||
name: t("generalSettings"),
|
||||
component: <GeneralSettings form={methods} />,
|
||||
component: <GeneralSettings />,
|
||||
},
|
||||
{
|
||||
name: t("capabilityConfig"),
|
||||
component: <CapabilityConfig form={methods} />,
|
||||
component: <CapabilityConfig />,
|
||||
},
|
||||
]}
|
||||
footer={<Footer />}
|
||||
onSave={() => save()}
|
||||
/>
|
||||
</FormProvider>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
|
|
88
src/clients/advanced/AddHostDialog.tsx
Normal file
88
src/clients/advanced/AddHostDialog.tsx
Normal file
|
@ -0,0 +1,88 @@
|
|||
import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Form,
|
||||
FormGroup,
|
||||
Modal,
|
||||
TextInput,
|
||||
} from "@patternfly/react-core";
|
||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
|
||||
type Host = {
|
||||
node: string;
|
||||
};
|
||||
|
||||
type AddHostDialogProps = {
|
||||
clientId: string;
|
||||
isOpen: boolean;
|
||||
onAdded: (host: string) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const AddHostDialog = ({
|
||||
clientId: id,
|
||||
isOpen,
|
||||
onAdded,
|
||||
onClose,
|
||||
}: AddHostDialogProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const { register, getValues } = useForm<Host>();
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={t("addNode")}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
variant="small"
|
||||
actions={[
|
||||
<Button
|
||||
id="add-node-confirm"
|
||||
key="confirm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
const node = getValues("node");
|
||||
await adminClient.clients.addClusterNode({
|
||||
id,
|
||||
node,
|
||||
});
|
||||
onAdded(node);
|
||||
addAlert(t("addedNodeSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("addedNodeFail", {
|
||||
error: error.response?.data?.error || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
}
|
||||
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>,
|
||||
<Button
|
||||
id="add-node-cancel"
|
||||
key="cancel"
|
||||
variant={ButtonVariant.secondary}
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form isHorizontal>
|
||||
<FormGroup label={t("nodeHost")} fieldId="nodeHost">
|
||||
<TextInput id="nodeHost" ref={register} name="node" />
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
162
src/clients/advanced/AdvancedSettings.tsx
Normal file
162
src/clients/advanced/AdvancedSettings.tsx
Normal file
|
@ -0,0 +1,162 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
Switch,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { TimeSelector } from "../../components/time-selector/TimeSelector";
|
||||
|
||||
type AdvancedSettingsProps = {
|
||||
control: Control<Record<string, any>>;
|
||||
save: () => void;
|
||||
reset: () => void;
|
||||
protocol?: string;
|
||||
};
|
||||
|
||||
export const AdvancedSettings = ({
|
||||
control,
|
||||
save,
|
||||
reset,
|
||||
protocol,
|
||||
}: AdvancedSettingsProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<FormAccess role="manage-realm" isHorizontal>
|
||||
{protocol !== "openid-connect" && (
|
||||
<FormGroup
|
||||
label={t("assertionLifespan")}
|
||||
fieldId="assertionLifespan"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:assertionLifespan"
|
||||
forLabel={t("assertionLifespan")}
|
||||
forID="assertionLifespan"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.saml-assertion-lifespan"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<TimeSelector
|
||||
units={["minutes", "days", "hours"]}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
{protocol === "openid-connect" && (
|
||||
<>
|
||||
<FormGroup
|
||||
label={t("accessTokenLifespan")}
|
||||
fieldId="accessTokenLifespan"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:accessTokenLifespan"
|
||||
forLabel={t("accessTokenLifespan")}
|
||||
forID="accessTokenLifespan"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.access-token-lifespan"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<TimeSelector
|
||||
units={["minutes", "days", "hours"]}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={t("oAuthMutual")}
|
||||
fieldId="oAuthMutual"
|
||||
hasNoPaddingTop
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:oAuthMutual"
|
||||
forLabel={t("oAuthMutual")}
|
||||
forID="oAuthMutual"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.tls-client-certificate-bound-access-tokens"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="oAuthMutual"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value === "true"}
|
||||
onChange={(value) => onChange("" + value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("keyForCodeExchange")}
|
||||
fieldId="keyForCodeExchange"
|
||||
hasNoPaddingTop
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:keyForCodeExchange"
|
||||
forLabel={t("keyForCodeExchange")}
|
||||
forID="keyForCodeExchange"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.pkce-code-challenge-method"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="keyForCodeExchange"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() => setOpen(!open)}
|
||||
isOpen={open}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setOpen(false);
|
||||
}}
|
||||
selections={[value]}
|
||||
>
|
||||
{["", "S256", "plain"].map((v) => (
|
||||
<SelectOption key={v} value={v} />
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
<ActionGroup>
|
||||
<Button variant="tertiary" onClick={save}>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button variant="link" onClick={reset}>
|
||||
{t("common:reload")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
137
src/clients/advanced/AuthenticationOverrides.tsx
Normal file
137
src/clients/advanced/AuthenticationOverrides.tsx
Normal file
|
@ -0,0 +1,137 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import {
|
||||
asyncStateFetch,
|
||||
useAdminClient,
|
||||
} from "../../context/auth/AdminClient";
|
||||
import { SaveReset } from "./SaveReset";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
|
||||
type AuthenticationOverridesProps = {
|
||||
control: Control<Record<string, any>>;
|
||||
save: () => void;
|
||||
reset: () => void;
|
||||
protocol?: string;
|
||||
};
|
||||
|
||||
export const AuthenticationOverrides = ({
|
||||
protocol,
|
||||
control,
|
||||
save,
|
||||
reset,
|
||||
}: AuthenticationOverridesProps) => {
|
||||
const adminClient = useAdminClient();
|
||||
const { t } = useTranslation("clients");
|
||||
const [flows, setFlows] = useState<JSX.Element[]>([]);
|
||||
const handleError = useErrorHandler();
|
||||
const [browserFlowOpen, setBrowserFlowOpen] = useState(false);
|
||||
const [directGrantOpen, setDirectGrantOpen] = useState(false);
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
asyncStateFetch(
|
||||
() => adminClient.authenticationManagement.getFlows(),
|
||||
(flows) => {
|
||||
let filteredFlows = [
|
||||
...flows.filter((flow) => flow.providerId !== "client-flow"),
|
||||
];
|
||||
filteredFlows = _.sortBy(filteredFlows, [(f) => f.alias]);
|
||||
setFlows([
|
||||
<SelectOption key="empty" value="">
|
||||
{t("common:choose")}
|
||||
</SelectOption>,
|
||||
...filteredFlows.map((flow) => (
|
||||
<SelectOption key={flow.id} value={flow.id}>
|
||||
{flow.alias}
|
||||
</SelectOption>
|
||||
)),
|
||||
]);
|
||||
},
|
||||
handleError
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormAccess role="manage-clients" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("browserFlow")}
|
||||
fieldId="browserFlow"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:browserFlow"
|
||||
forLabel={t("browserFlow")}
|
||||
forID="browserFlow"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="authenticationFlowBindingOverrides.browser"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="browserFlow"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() => setBrowserFlowOpen(!browserFlowOpen)}
|
||||
isOpen={browserFlowOpen}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setBrowserFlowOpen(false);
|
||||
}}
|
||||
selections={[value]}
|
||||
>
|
||||
{flows}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{protocol === "openid-connect" && (
|
||||
<FormGroup
|
||||
label={t("directGrant")}
|
||||
fieldId="directGrant"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:directGrant"
|
||||
forLabel={t("directGrant")}
|
||||
forID="directGrant"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="authenticationFlowBindingOverrides.direct_grant"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="directGrant"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() => setDirectGrantOpen(!directGrantOpen)}
|
||||
isOpen={directGrantOpen}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setDirectGrantOpen(false);
|
||||
}}
|
||||
selections={[value]}
|
||||
>
|
||||
{flows}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
)}
|
||||
<SaveReset name="authenticationOverrides" save={save} reset={reset} />
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
357
src/clients/advanced/FineGrainOpenIdConnect.tsx
Normal file
357
src/clients/advanced/FineGrainOpenIdConnect.tsx
Normal file
|
@ -0,0 +1,357 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||
import { sortProviders } from "../../util";
|
||||
|
||||
type FineGrainOpenIdConnectProps = {
|
||||
control: Control<Record<string, any>>;
|
||||
save: () => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const FineGrainOpenIdConnect = ({
|
||||
control,
|
||||
save,
|
||||
reset,
|
||||
}: FineGrainOpenIdConnectProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const providers = useServerInfo().providers;
|
||||
const clientSignatureProviders = providers?.clientSignature.providers;
|
||||
const contentEncryptionProviders = providers?.contentencryption.providers;
|
||||
const cekManagementProviders = providers?.cekmanagement.providers;
|
||||
const signatureProviders = providers?.signature.providers;
|
||||
const [accessTokenOpen, setAccessTokenOpen] = useState(false);
|
||||
const [idTokenOpen, setIdTokenOpen] = useState(false);
|
||||
const [idTokenKeyManagementOpen, setIdTokenKeyManagementOpen] = useState(
|
||||
false
|
||||
);
|
||||
const [idTokenContentOpen, setIdTokenContentOpen] = useState(false);
|
||||
const [userInfoSignedResponseOpen, setUserInfoSignedResponseOpen] = useState(
|
||||
false
|
||||
);
|
||||
const [requestObjectSignatureOpen, setRequestObjectSignatureOpen] = useState(
|
||||
false
|
||||
);
|
||||
const [requestObjectRequiredOpen, setRequestObjectRequiredOpen] = useState(
|
||||
false
|
||||
);
|
||||
|
||||
const keyOptions = [
|
||||
<SelectOption key="empty" value="">
|
||||
{t("common:choose")}
|
||||
</SelectOption>,
|
||||
...sortProviders(clientSignatureProviders!).map((p) => (
|
||||
<SelectOption key={p} value={p} />
|
||||
)),
|
||||
];
|
||||
const cekManagementOptions = [
|
||||
<SelectOption key="empty" value="">
|
||||
{t("common:choose")}
|
||||
</SelectOption>,
|
||||
...sortProviders(cekManagementProviders!).map((p) => (
|
||||
<SelectOption key={p} value={p} />
|
||||
)),
|
||||
];
|
||||
const signatureOptions = [
|
||||
<SelectOption key="unsigned" value="">
|
||||
{t("unsigned")}
|
||||
</SelectOption>,
|
||||
...sortProviders(signatureProviders!).map((p) => (
|
||||
<SelectOption key={p} value={p} />
|
||||
)),
|
||||
];
|
||||
const contentOptions = [
|
||||
<SelectOption key="empty" value="">
|
||||
{t("common:choose")}
|
||||
</SelectOption>,
|
||||
...sortProviders(contentEncryptionProviders!).map((p) => (
|
||||
<SelectOption key={p} value={p} />
|
||||
)),
|
||||
];
|
||||
|
||||
const requestObjectOptions = [
|
||||
<SelectOption key="any" value="any">
|
||||
{t("any")}
|
||||
</SelectOption>,
|
||||
<SelectOption key="none" value="none">
|
||||
{t("none")}
|
||||
</SelectOption>,
|
||||
...sortProviders(clientSignatureProviders!).map((p) => (
|
||||
<SelectOption key={p} value={p} />
|
||||
)),
|
||||
];
|
||||
|
||||
const requestObjectRequiredOptions = [
|
||||
"not required",
|
||||
"request or request_uri",
|
||||
"request only",
|
||||
"request_uri only",
|
||||
].map((p) => (
|
||||
<SelectOption key={p} value={p}>
|
||||
{t(`requestObject.${p}`)}
|
||||
</SelectOption>
|
||||
));
|
||||
|
||||
const selectOptionToString = (value: string, options: JSX.Element[]) => {
|
||||
const selectOption = options.find((s) => s.props.value === value);
|
||||
return selectOption?.props.children || selectOption?.props.value;
|
||||
};
|
||||
return (
|
||||
<FormAccess role="manage-clients" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("accessTokenSignatureAlgorithm")}
|
||||
fieldId="accessTokenSignatureAlgorithm"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:accessTokenSignatureAlgorithm"
|
||||
forLabel={t("accessTokenSignatureAlgorithm")}
|
||||
forID="accessTokenSignatureAlgorithm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.access-token-signed-response-alg"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="accessTokenSignatureAlgorithm"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() => setAccessTokenOpen(!accessTokenOpen)}
|
||||
isOpen={accessTokenOpen}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setAccessTokenOpen(false);
|
||||
}}
|
||||
selections={[selectOptionToString(value, keyOptions)]}
|
||||
>
|
||||
{keyOptions}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("idTokenSignatureAlgorithm")}
|
||||
fieldId="kc-id-token-signature"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:idTokenSignatureAlgorithm"
|
||||
forLabel={t("idTokenSignatureAlgorithm")}
|
||||
forID="idTokenSignatureAlgorithm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.id-token-signed-response-alg"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="idTokenSignatureAlgorithm"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() => setIdTokenOpen(!idTokenOpen)}
|
||||
isOpen={idTokenOpen}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setIdTokenOpen(false);
|
||||
}}
|
||||
selections={[selectOptionToString(value, keyOptions)]}
|
||||
>
|
||||
{keyOptions}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("idTokenEncryptionKeyManagementAlgorithm")}
|
||||
fieldId="idTokenEncryptionKeyManagementAlgorithm"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:idTokenEncryptionKeyManagementAlgorithm"
|
||||
forLabel={t("idTokenEncryptionKeyManagementAlgorithm")}
|
||||
forID="idTokenEncryptionKeyManagementAlgorithm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.id-token-encrypted-response-alg"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="idTokenEncryptionKeyManagementAlgorithm"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() =>
|
||||
setIdTokenKeyManagementOpen(!idTokenKeyManagementOpen)
|
||||
}
|
||||
isOpen={idTokenKeyManagementOpen}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setIdTokenKeyManagementOpen(false);
|
||||
}}
|
||||
selections={[selectOptionToString(value, cekManagementOptions)]}
|
||||
>
|
||||
{cekManagementOptions}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("idTokenEncryptionContentEncryptionAlgorithm")}
|
||||
fieldId="idTokenEncryptionContentEncryptionAlgorithm"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:idTokenEncryptionContentEncryptionAlgorithm"
|
||||
forLabel={t("idTokenEncryptionContentEncryptionAlgorithm")}
|
||||
forID="idTokenEncryptionContentEncryptionAlgorithm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.id-token-encrypted-response-enc"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="idTokenEncryptionContentEncryptionAlgorithm"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() => setIdTokenContentOpen(!idTokenContentOpen)}
|
||||
isOpen={idTokenContentOpen}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setIdTokenContentOpen(false);
|
||||
}}
|
||||
selections={[selectOptionToString(value, contentOptions)]}
|
||||
>
|
||||
{contentOptions}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("userInfoSignedResponseAlgorithm")}
|
||||
fieldId="userInfoSignedResponseAlgorithm"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:userInfoSignedResponseAlgorithm"
|
||||
forLabel={t("userInfoSignedResponseAlgorithm")}
|
||||
forID="userInfoSignedResponseAlgorithm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.user-info-response-signature-alg"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="userInfoSignedResponseAlgorithm"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() =>
|
||||
setUserInfoSignedResponseOpen(!userInfoSignedResponseOpen)
|
||||
}
|
||||
isOpen={userInfoSignedResponseOpen}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setUserInfoSignedResponseOpen(false);
|
||||
}}
|
||||
selections={[selectOptionToString(value, signatureOptions)]}
|
||||
>
|
||||
{signatureOptions}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("requestObjectSignatureAlgorithm")}
|
||||
fieldId="requestObjectSignatureAlgorithm"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:requestObjectSignatureAlgorithm"
|
||||
forLabel={t("requestObjectSignatureAlgorithm")}
|
||||
forID="requestObjectSignatureAlgorithm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.request_object_signature_alg"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="requestObjectSignatureAlgorithm"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() =>
|
||||
setRequestObjectSignatureOpen(!requestObjectSignatureOpen)
|
||||
}
|
||||
isOpen={requestObjectSignatureOpen}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setRequestObjectSignatureOpen(false);
|
||||
}}
|
||||
selections={[selectOptionToString(value, requestObjectOptions)]}
|
||||
>
|
||||
{requestObjectOptions}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("requestObjectRequired")}
|
||||
fieldId="requestObjectRequired"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:requestObjectRequired"
|
||||
forLabel={t("requestObjectRequired")}
|
||||
forID="requestObjectRequired"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.request-object-required"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="requestObjectRequired"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() =>
|
||||
setRequestObjectRequiredOpen(!requestObjectRequiredOpen)
|
||||
}
|
||||
isOpen={requestObjectRequiredOpen}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setRequestObjectRequiredOpen(false);
|
||||
}}
|
||||
selections={[
|
||||
selectOptionToString(value, requestObjectRequiredOptions),
|
||||
]}
|
||||
>
|
||||
{requestObjectRequiredOptions}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button id="fineGrainSave" variant="tertiary" onClick={save}>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button id="fineGrainReload" variant="link" onClick={reset}>
|
||||
{t("common:reload")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
111
src/clients/advanced/FineGrainSamlEndpointConfig.tsx
Normal file
111
src/clients/advanced/FineGrainSamlEndpointConfig.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Control } from "react-hook-form";
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
FormGroup,
|
||||
TextInput,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
|
||||
type FineGrainSamlEndpointConfigProps = {
|
||||
control: Control<Record<string, any>>;
|
||||
save: () => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const FineGrainSamlEndpointConfig = ({
|
||||
control: { register },
|
||||
save,
|
||||
reset,
|
||||
}: FineGrainSamlEndpointConfigProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
return (
|
||||
<FormAccess role="manage-realm" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("assertionConsumerServicePostBindingURL")}
|
||||
fieldId="assertionConsumerServicePostBindingURL"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:assertionConsumerServicePostBindingURL"
|
||||
forLabel={t("assertionConsumerServicePostBindingURL")}
|
||||
forID="assertionConsumerServicePostBindingURL"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="assertionConsumerServicePostBindingURL"
|
||||
name="attributes.saml_assertion_consumer_url_post"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("assertionConsumerServiceRedirectBindingURL")}
|
||||
fieldId="assertionConsumerServiceRedirectBindingURL"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:assertionConsumerServiceRedirectBindingURL"
|
||||
forLabel={t("assertionConsumerServiceRedirectBindingURL")}
|
||||
forID="assertionConsumerServiceRedirectBindingURL"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="assertionConsumerServiceRedirectBindingURL"
|
||||
name="attributes.saml_assertion_consumer_url_redirect"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("logoutServicePostBindingURL")}
|
||||
fieldId="logoutServicePostBindingURL"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:logoutServicePostBindingURL"
|
||||
forLabel={t("logoutServicePostBindingURL")}
|
||||
forID="logoutServicePostBindingURL"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="logoutServicePostBindingURL"
|
||||
name="attributes.saml_single_logout_service_url_post"
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("logoutServiceRedirectBindingURL")}
|
||||
fieldId="logoutServiceRedirectBindingURL"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:logoutServiceRedirectBindingURL"
|
||||
forLabel={t("logoutServiceRedirectBindingURL")}
|
||||
forID="logoutServiceRedirectBindingURL"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="logoutServiceRedirectBindingURL"
|
||||
name="attributes.saml_single_logout_service_url_redirect"
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<ActionGroup>
|
||||
<Button variant="tertiary" onClick={save}>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button variant="link" onClick={reset}>
|
||||
{t("common:reload")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
59
src/clients/advanced/OpenIdConnectCompatibilityModes.tsx
Normal file
59
src/clients/advanced/OpenIdConnectCompatibilityModes.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import { ActionGroup, Button, FormGroup, Switch } from "@patternfly/react-core";
|
||||
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
|
||||
type OpenIdConnectCompatibilityModesProps = {
|
||||
control: Control<Record<string, any>>;
|
||||
save: () => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const OpenIdConnectCompatibilityModes = ({
|
||||
control,
|
||||
save,
|
||||
reset,
|
||||
}: OpenIdConnectCompatibilityModesProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
return (
|
||||
<FormAccess role="manage-realm" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("excludeSessionStateFromAuthenticationResponse")}
|
||||
fieldId="excludeSessionStateFromAuthenticationResponse"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:excludeSessionStateFromAuthenticationResponse"
|
||||
forLabel={t("excludeSessionStateFromAuthenticationResponse")}
|
||||
forID="excludeSessionStateFromAuthenticationResponse"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.exclude-session-state-from-auth-response"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
id="excludeSessionStateFromAuthenticationResponse"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={value === "true"}
|
||||
onChange={(value) => onChange("" + value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button variant="tertiary" onClick={save}>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button variant="link" onClick={reset}>
|
||||
{t("common:reload")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
);
|
||||
};
|
23
src/clients/advanced/SaveReset.tsx
Normal file
23
src/clients/advanced/SaveReset.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActionGroup, Button } from "@patternfly/react-core";
|
||||
|
||||
type SaveResetProps = {
|
||||
name: string;
|
||||
save: () => void;
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
export const SaveReset = ({ name, save, reset }: SaveResetProps) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<ActionGroup>
|
||||
<Button data-testid={name + "Save"} variant="tertiary" onClick={save}>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button data-testid={name + "Reload"} variant="link" onClick={reset}>
|
||||
{t("common:reload")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
);
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { Controller, UseFormMethods, useWatch } from "react-hook-form";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import {
|
||||
|
@ -48,17 +48,20 @@ type AccessToken = {
|
|||
|
||||
export type CredentialsProps = {
|
||||
clientId: string;
|
||||
form: UseFormMethods;
|
||||
save: () => void;
|
||||
};
|
||||
|
||||
export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
|
||||
export const Credentials = ({ clientId, save }: CredentialsProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const handleError = useErrorHandler();
|
||||
const { addAlert } = useAlerts();
|
||||
const {
|
||||
control,
|
||||
formState: { isDirty },
|
||||
} = useFormContext();
|
||||
const clientAuthenticatorType = useWatch({
|
||||
control: form.control,
|
||||
control: control,
|
||||
name: "clientAuthenticatorType",
|
||||
});
|
||||
|
||||
|
@ -158,7 +161,7 @@ export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
|
|||
>
|
||||
<Controller
|
||||
name="clientAuthenticatorType"
|
||||
control={form.control}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="kc-client-authenticator-type"
|
||||
|
@ -186,15 +189,13 @@ export const Credentials = ({ clientId, form, save }: CredentialsProps) => {
|
|||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
{clientAuthenticatorType === "client-jwt" && (
|
||||
<SignedJWT form={form} />
|
||||
)}
|
||||
{clientAuthenticatorType === "client-x509" && <X509 form={form} />}
|
||||
{clientAuthenticatorType === "client-jwt" && <SignedJWT />}
|
||||
{clientAuthenticatorType === "client-x509" && <X509 />}
|
||||
<ActionGroup>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => save()}
|
||||
isDisabled={!form.formState.isDirty}
|
||||
isDisabled={!isDirty}
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, UseFormMethods } from "react-hook-form";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import {
|
||||
FormGroup,
|
||||
Select,
|
||||
|
@ -12,11 +12,8 @@ import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
|||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { sortProviders } from "../../util";
|
||||
|
||||
export type SignedJWTProps = {
|
||||
form: UseFormMethods;
|
||||
};
|
||||
|
||||
export const SignedJWT = ({ form }: SignedJWTProps) => {
|
||||
export const SignedJWT = () => {
|
||||
const { control } = useFormContext();
|
||||
const providers = sortProviders(
|
||||
useServerInfo().providers!.clientSignature.providers
|
||||
);
|
||||
|
@ -37,9 +34,9 @@ export const SignedJWT = ({ form }: SignedJWTProps) => {
|
|||
}
|
||||
>
|
||||
<Controller
|
||||
name="attributes.token_endpoint_auth_signing_alg"
|
||||
name="attributes.token-endpoint-auth-signing-alg"
|
||||
defaultValue={providers[0]}
|
||||
control={form.control}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="kc-signature-algorithm"
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UseFormMethods } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import { FormGroup, TextInput } from "@patternfly/react-core";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
|
||||
export type X509Props = {
|
||||
form: UseFormMethods;
|
||||
};
|
||||
|
||||
export const X509 = ({ form }: X509Props) => {
|
||||
export const X509 = () => {
|
||||
const { t } = useTranslation("clients");
|
||||
const { register } = useFormContext();
|
||||
return (
|
||||
<FormGroup
|
||||
label={t("subject")}
|
||||
|
@ -23,10 +20,10 @@ export const X509 = ({ form }: X509Props) => {
|
|||
}
|
||||
>
|
||||
<TextInput
|
||||
ref={form.register()}
|
||||
ref={register()}
|
||||
type="text"
|
||||
id="kc-subject"
|
||||
name="attributes.x509_subjectdn"
|
||||
name="attributes.x509-subjectdn"
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
|
|
|
@ -10,6 +10,33 @@
|
|||
"subject": "A regular expression for validating Subject DN in the Client Certificate. Use \"(.*?)(?:$)\" to match all kind of expressions.",
|
||||
"evaluateExplain": "This page allows you to see all protocol mappers and role scope mappings",
|
||||
"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"
|
||||
"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",
|
||||
"notBefore": "Revoke any tokens issued before this date for this client.",
|
||||
"notBeforeIntro": "In order to successfully push a revocation policy to the client, you need to set an Admin URL under the <1>Settings</1> tab for this client first",
|
||||
"nodeReRegistrationTimeout": "Interval to specify max time for registered clients cluster nodes to re-register. If cluster node will not send re-registration request to Keycloak within this time, it will be unregistered from Keycloak",
|
||||
"fineGrainOpenIdConnectConfiguration": "This section is used to configure advanced settings of this client related to OpenID connect protocol.",
|
||||
"fineGrainSamlEndpointConfig": "This section to configure exact URLs for Assertion Consumer and Single Logout Service.",
|
||||
"accessTokenSignatureAlgorithm": "JWA algorithm used for signing access tokens.",
|
||||
"idTokenSignatureAlgorithm": "JWA algorithm used for signing ID tokens.",
|
||||
"idTokenEncryptionKeyManagementAlgorithm": "JWA Algorithm used for key management in encrypting ID tokens. This option is needed if you want encrypted ID tokens. If left empty, ID Tokens are just signed, but not encrypted.",
|
||||
"idTokenEncryptionContentEncryptionAlgorithm": "JWA Algorithm used for content encryption in encrypting ID tokens. This option is needed just if you want encrypted ID tokens. If left empty, ID Tokens are just signed, but not encrypted.",
|
||||
"userInfoSignedResponseAlgorithm": "JWA algorithm used for signed User Info Endpoint response. If set to 'unsigned', User Info Response won't be signed and will be returned in application/json format.",
|
||||
"requestObjectSignatureAlgorithm": "JWA algorithm, which client needs to use when sending OIDC request object specified by 'request' or 'request_uri' parameters. If set to 'any', Request object can be signed by any algorithm (including 'none' ).",
|
||||
"requestObjectRequired": "Specifies if the client needs to provide a request object with their authorization requests, and what method they can use for this. If set to \"not required\", providing a request object is optional. In all other cases, providing a request object is mandatory. If set to \"request\", the request object must be provided by value. If set to \"request_uri\", the request object must be provided by reference. If set to \"request or request_uri\", either method can be used.",
|
||||
"openIdConnectCompatibilityModes": "This section is used to configure settings for backward compatibility with older OpenID Connect / OAuth 2 adaptors. It's useful especially if your client uses older version of Keycloak / RH-SSO adapter.",
|
||||
"excludeSessionStateFromAuthenticationResponse": "If this is on, the parameter 'session_state' will not be included in OpenID Connect Authentication Response. It is useful if your client uses older OIDC / OAuth2 adapter, which does not support 'session_state' parameter.",
|
||||
"advancedSettingsOpenid-connect": "This section is used to configure advanced settings of this client related to OpenID Connect protocol",
|
||||
"advancedSettingsSaml": "This section is used to configure advanced settings of this client",
|
||||
"assertionLifespan": "Lifespan set in the SAML assertion conditions. After that time the assertion will be invalid. The \"SessionNotOnOrAfter\" attribute is not modified and continue using the \"SSO Session Max\" time defined at realm level.",
|
||||
"accessTokenLifespan": "Max time before an access token is expired. This value is recommended to be short relative to the SSO timeout.",
|
||||
"oAuthMutual": "This enables support for OAuth 2.0 Mutual TLS Certificate Bound Access Tokens, which means that keycloak bind an access token and a refresh token with a X.509 certificate of a token requesting client exchanged in mutual TLS between keycloak's Token Endpoint and this client. These tokens can be treated as Holder-of-Key tokens instead of bearer tokens.",
|
||||
"keyForCodeExchange": "Choose which code challenge method for PKCE is used. If not specified, keycloak does not applies PKCE to a client unless the client sends an authorization request with appropriate code challenge and code exchange method.",
|
||||
"assertionConsumerServicePostBindingURL": "SAML POST Binding URL for the client's assertion consumer service (login responses). You can leave this blank if you do not have a URL for this binding.",
|
||||
"assertionConsumerServiceRedirectBindingURL": "SAML Redirect Binding URL for the client's assertion consumer service (login responses). You can leave this blank if you do not have a URL for this binding.",
|
||||
"logoutServicePostBindingURL": "SAML POST Binding URL for the client's single logout service. You can leave this blank if you are using a different binding",
|
||||
"logoutServiceRedirectBindingURL": "SAML Redirect Binding URL for the client's single logout service. You can leave this blank if you are using a different binding.",
|
||||
"authenticationOverrides": "Override realm authentication flow bindings.",
|
||||
"browserFlow": "Select the flow you want to use for browser authentication.",
|
||||
"directGrant": "Select the flow you want to use for direct grant authentication."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
Button,
|
||||
AlertVariant,
|
||||
} from "@patternfly/react-core";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ClientDescription } from "../ClientDescription";
|
||||
|
@ -60,8 +60,9 @@ export const ImportForm = () => {
|
|||
onSubmit={handleSubmit(save)}
|
||||
role="manage-clients"
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<JsonFileUpload id="realm-file" onChange={handleFileChange} />
|
||||
<ClientDescription form={form} />
|
||||
<ClientDescription />
|
||||
<FormGroup label={t("common:type")} fieldId="kc-type">
|
||||
<TextInput
|
||||
type="text"
|
||||
|
@ -77,6 +78,7 @@ export const ImportForm = () => {
|
|||
</Button>
|
||||
<Button variant="link">{t("common:cancel")}</Button>
|
||||
</ActionGroup>
|
||||
</FormProvider>
|
||||
</FormAccess>
|
||||
</PageSection>
|
||||
</>
|
||||
|
|
|
@ -100,6 +100,63 @@
|
|||
"accessTokenError": "Could not regenerate access token due to: {{error}}",
|
||||
"signatureAlgorithm": "Signature algorithm",
|
||||
"subject": "Subject DN",
|
||||
"searchForClient": "Search for client"
|
||||
"searchForClient": "Search for client",
|
||||
"advanced": "Advanced",
|
||||
"revocation": "Revocation",
|
||||
"clustering": "Clustering",
|
||||
"notBefore": "Not before",
|
||||
"setToNow": "Set to now",
|
||||
"noAdminUrlSet": "No push sent. No admin URI configured or no registered cluster nodes available",
|
||||
"notBeforePushFail": "Failed to push \"not before\" to: {{failedNodes}}",
|
||||
"notBeforePushSuccess": "Successfully push \"not before\" to: {{successNodes}}",
|
||||
"testClusterFail": "Failed verified availability for: {{failedNodes}}. Fix or unregister failed cluster nodes and try again",
|
||||
"testClusterSuccess": "Successfully verified availability for: {{successNodes}}",
|
||||
"deleteNode": "Delete node?",
|
||||
"deleteNodeBody": "Are you sure you want to permanently delete the node \"{{node}}\"",
|
||||
"deleteNodeSuccess": "Node successfully removed",
|
||||
"deleteNodeFail": "Could not delete node: '{{error}}'",
|
||||
"addedNodeSuccess": "Node successfully added",
|
||||
"addedNodeFail": "Could not add node: '{{error}}'",
|
||||
"addNode": "Add node",
|
||||
"push": "Push",
|
||||
"clear": "Clear",
|
||||
"none": "None",
|
||||
"any": "Any",
|
||||
"nodeReRegistrationTimeout": "Node Re-registration timeout",
|
||||
"registeredClusterNodes": "Registered cluster nodes",
|
||||
"nodeHost": "Node host",
|
||||
"lastRegistration": "Last registration",
|
||||
"testClusterAvailability": "Test cluster availability",
|
||||
"registerNodeManually": "Register node manually",
|
||||
"fineGrainOpenIdConnectConfiguration": "Fine grain OpenID connect configuration",
|
||||
"fineGrainSamlEndpointConfig": "Fine Grain SAML Endpoint Configuration",
|
||||
"accessTokenSignatureAlgorithm": "Access token signature algorithm",
|
||||
"idTokenSignatureAlgorithm": "ID token signature algorithm",
|
||||
"idTokenEncryptionKeyManagementAlgorithm": "ID token encryption key management algorithm",
|
||||
"idTokenEncryptionContentEncryptionAlgorithm": "ID token encryption content encryption algorithm",
|
||||
"userInfoSignedResponseAlgorithm": "User info signed response algorithm",
|
||||
"requestObjectSignatureAlgorithm": "Request object signature algorithm",
|
||||
"requestObjectRequired": "Request object required",
|
||||
"requestObject": {
|
||||
"not required": "Not required",
|
||||
"request or request_uri": "Request or Request URI",
|
||||
"request only": "Request only",
|
||||
"request_uri only": "Request URI only"
|
||||
},
|
||||
"openIdConnectCompatibilityModes": "Open ID Connect Compatibly Modes",
|
||||
"excludeSessionStateFromAuthenticationResponse": "Exclude Session State From Authentication Response",
|
||||
"assertionConsumerServicePostBindingURL": "Assertion Consumer Service POST Binding URL",
|
||||
"assertionConsumerServiceRedirectBindingURL" :"Assertion Consumer Service Redirect Binding URL",
|
||||
"logoutServicePostBindingURL": "Logout Service POST Binding URL",
|
||||
"logoutServiceRedirectBindingURL": "Logout Service Redirect Binding URL",
|
||||
"advancedSettings": "Advanced Settings",
|
||||
"assertionLifespan": "Assertion Lifespan",
|
||||
"accessTokenLifespan": "Access Token Lifespan",
|
||||
"oAuthMutual": "OAuth 2.0 Mutual TLS Certificate Bound Access Tokens Enabled",
|
||||
"keyForCodeExchange": "Proof Key for Code Exchange Code Challenge Method",
|
||||
"authenticationOverrides": "Authentication flow overrides",
|
||||
"browserFlow": "Browser Flow",
|
||||
"directGrant": "Direct Grant Flow"
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -325,7 +325,10 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
|
|||
setUserSearch(value);
|
||||
return userItems;
|
||||
}}
|
||||
onClear={() => setUser(undefined)}
|
||||
onClear={() => {
|
||||
setUser(undefined);
|
||||
setUserSearch("");
|
||||
}}
|
||||
selections={[user]}
|
||||
onSelect={(_, value) => {
|
||||
setUser(value as UserRepresentation);
|
||||
|
|
|
@ -84,6 +84,14 @@
|
|||
"Wednesday": "Wednesday",
|
||||
"Thursday": "Thursday",
|
||||
"Friday": "Friday",
|
||||
"Saturday": "Saturday"
|
||||
"Saturday": "Saturday",
|
||||
|
||||
"unitLabel": "Select a time unit",
|
||||
"times": {
|
||||
"seconds": "Seconds",
|
||||
"minutes": "Minutes",
|
||||
"hours": "Hours",
|
||||
"days": "Days"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, ReactElement, useContext } from "react";
|
||||
import React, { useState, useEffect, useContext } from "react";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import {
|
||||
Alert,
|
||||
|
@ -25,37 +25,19 @@ import {
|
|||
} from "../../context/auth/AdminClient";
|
||||
import { HelpContext } from "../help-enabler/HelpHeader";
|
||||
|
||||
export type DownloadDialogProps = {
|
||||
type DownloadDialogProps = {
|
||||
id: string;
|
||||
protocol?: string;
|
||||
};
|
||||
|
||||
type DownloadDialogModalProps = DownloadDialogProps & {
|
||||
open: boolean;
|
||||
toggleDialog: () => void;
|
||||
};
|
||||
|
||||
export const useDownloadDialog = (
|
||||
props: DownloadDialogProps
|
||||
): [() => void, () => ReactElement] => {
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
function toggleDialog() {
|
||||
setShow((show) => !show);
|
||||
}
|
||||
|
||||
const Dialog = () => (
|
||||
<DownloadDialog {...props} open={show} toggleDialog={toggleDialog} />
|
||||
);
|
||||
return [toggleDialog, Dialog];
|
||||
};
|
||||
|
||||
export const DownloadDialog = ({
|
||||
id,
|
||||
open,
|
||||
toggleDialog,
|
||||
protocol = "openid-connect",
|
||||
}: DownloadDialogModalProps) => {
|
||||
}: DownloadDialogProps) => {
|
||||
const adminClient = useAdminClient();
|
||||
const handleError = useErrorHandler();
|
||||
const { t } = useTranslation("common");
|
||||
|
@ -85,7 +67,7 @@ export const DownloadDialog = ({
|
|||
(snippet) => setSnippet(snippet),
|
||||
handleError
|
||||
);
|
||||
}, [selected]);
|
||||
}, [id, selected]);
|
||||
return (
|
||||
<ConfirmDialogModal
|
||||
titleKey={t("clients:downloadAdaptorTitle")}
|
||||
|
|
|
@ -3,6 +3,7 @@ import React, {
|
|||
cloneElement,
|
||||
isValidElement,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import { Controller } from "react-hook-form";
|
||||
import {
|
||||
|
@ -39,7 +40,7 @@ export type FormAccessProps = FormProps & {
|
|||
* @type {boolean}
|
||||
*/
|
||||
unWrap?: boolean;
|
||||
children: ReactElement[];
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -56,9 +57,9 @@ export const FormAccess = ({
|
|||
const { hasAccess } = useAccess();
|
||||
|
||||
const recursiveCloneChildren = (
|
||||
children: ReactElement[],
|
||||
children: ReactNode,
|
||||
newProps: any
|
||||
): ReactElement[] => {
|
||||
): ReactNode => {
|
||||
return Children.map(children, (child) => {
|
||||
if (!isValidElement(child)) {
|
||||
return child;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect } from "react";
|
||||
import { useFieldArray, UseFormMethods } from "react-hook-form";
|
||||
import { useFieldArray, useFormContext, UseFormMethods } from "react-hook-form";
|
||||
import {
|
||||
TextInput,
|
||||
Split,
|
||||
|
@ -10,7 +10,7 @@ import {
|
|||
} from "@patternfly/react-core";
|
||||
import { MinusIcon, PlusIcon } from "@patternfly/react-icons";
|
||||
|
||||
type MultiLine = {
|
||||
export type MultiLine = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
|
@ -25,22 +25,17 @@ export function toValue(formValue: MultiLine[]): string[] {
|
|||
}
|
||||
|
||||
export type MultiLineInputProps = Omit<TextInputProps, "form"> & {
|
||||
form: UseFormMethods;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const MultiLineInput = ({
|
||||
name,
|
||||
form,
|
||||
...rest
|
||||
}: MultiLineInputProps) => {
|
||||
const { register, control } = form;
|
||||
export const MultiLineInput = ({ name, ...rest }: MultiLineInputProps) => {
|
||||
const { register, control, reset } = useFormContext();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
name,
|
||||
control,
|
||||
});
|
||||
useEffect(() => {
|
||||
form.reset({
|
||||
reset({
|
||||
[name]: [{ value: "" }],
|
||||
});
|
||||
}, []);
|
||||
|
|
|
@ -84,7 +84,7 @@ export type DataListProps<T> = {
|
|||
canSelectAll?: boolean;
|
||||
isPaginated?: boolean;
|
||||
ariaLabelKey: string;
|
||||
searchPlaceholderKey: string;
|
||||
searchPlaceholderKey?: string;
|
||||
columns: Field<T>[];
|
||||
actions?: Action<T>[];
|
||||
actionResolver?: IActionsResolver;
|
||||
|
@ -254,10 +254,12 @@ export function KeycloakDataTable<T>({
|
|||
setFirst(first);
|
||||
setMax(max);
|
||||
}}
|
||||
inputGroupName={`${ariaLabelKey}input`}
|
||||
inputGroupName={
|
||||
searchPlaceholderKey ? `${ariaLabelKey}input` : undefined
|
||||
}
|
||||
inputGroupOnChange={searchOnChange}
|
||||
inputGroupOnClick={refresh}
|
||||
inputGroupPlaceholder={t(searchPlaceholderKey)}
|
||||
inputGroupPlaceholder={t(searchPlaceholderKey || "")}
|
||||
searchTypeComponent={searchTypeComponent}
|
||||
toolbarItem={toolbarItem}
|
||||
>
|
||||
|
@ -278,10 +280,12 @@ export function KeycloakDataTable<T>({
|
|||
)}
|
||||
{rows && !isPaginated && (
|
||||
<TableToolbar
|
||||
inputGroupName={`${ariaLabelKey}input`}
|
||||
inputGroupName={
|
||||
searchPlaceholderKey ? `${ariaLabelKey}input` : undefined
|
||||
}
|
||||
inputGroupOnChange={searchOnChange}
|
||||
inputGroupOnClick={() => {}}
|
||||
inputGroupPlaceholder={t(searchPlaceholderKey)}
|
||||
inputGroupPlaceholder={t(searchPlaceholderKey || "")}
|
||||
toolbarItem={toolbarItem}
|
||||
searchTypeComponent={searchTypeComponent}
|
||||
>
|
||||
|
|
109
src/components/time-selector/TimeSelector.tsx
Normal file
109
src/components/time-selector/TimeSelector.tsx
Normal file
|
@ -0,0 +1,109 @@
|
|||
import {
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
Split,
|
||||
SplitItem,
|
||||
TextInput,
|
||||
} from "@patternfly/react-core";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type Unit = "seconds" | "minutes" | "hours" | "days";
|
||||
|
||||
export type TimeSelectorProps = {
|
||||
value: number;
|
||||
units?: Unit[];
|
||||
onChange: (time: number | string) => void;
|
||||
};
|
||||
|
||||
export const TimeSelector = ({
|
||||
value,
|
||||
units = ["seconds", "minutes", "hours", "days"],
|
||||
onChange,
|
||||
}: TimeSelectorProps) => {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
const allTimes: { unit: Unit; label: string; multiplier: number }[] = [
|
||||
{ unit: "seconds", label: t("times.seconds"), multiplier: 1 },
|
||||
{ unit: "minutes", label: t("times.minutes"), multiplier: 60 },
|
||||
{ unit: "hours", label: t("times.hours"), multiplier: 3600 },
|
||||
{ unit: "days", label: t("times.days"), multiplier: 86400 },
|
||||
];
|
||||
|
||||
const times = allTimes.filter((t) => units.includes(t.unit));
|
||||
const defaultMultiplier = allTimes.find((time) => time.unit === units[0])
|
||||
?.multiplier;
|
||||
|
||||
const [timeValue, setTimeValue] = useState<"" | number>("");
|
||||
const [multiplier, setMultiplier] = useState(defaultMultiplier);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const x = times.reduce(
|
||||
(v, time) =>
|
||||
value % time.multiplier === 0 && v < time.multiplier
|
||||
? time.multiplier
|
||||
: v,
|
||||
1
|
||||
);
|
||||
|
||||
if (value) {
|
||||
setMultiplier(x);
|
||||
setTimeValue(value / x);
|
||||
} else {
|
||||
setTimeValue("");
|
||||
setMultiplier(defaultMultiplier);
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const updateTimeout = (
|
||||
timeout: "" | number,
|
||||
times: number | undefined = multiplier
|
||||
) => {
|
||||
if (timeout !== "") {
|
||||
onChange(timeout * (times || 1));
|
||||
setTimeValue(timeout);
|
||||
} else {
|
||||
onChange("");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Split hasGutter>
|
||||
<SplitItem>
|
||||
<TextInput
|
||||
type="number"
|
||||
id={`kc-time-${new Date().getTime()}`}
|
||||
min="0"
|
||||
value={timeValue}
|
||||
onChange={(value) => {
|
||||
updateTimeout("" === value ? value : parseInt(value));
|
||||
}}
|
||||
/>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<Select
|
||||
variant={SelectVariant.single}
|
||||
aria-label={t("unitLabel")}
|
||||
onSelect={(_, value) => {
|
||||
setMultiplier(value as number);
|
||||
updateTimeout(timeValue, value as number);
|
||||
setOpen(false);
|
||||
}}
|
||||
selections={[multiplier]}
|
||||
onToggle={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
isOpen={open}
|
||||
>
|
||||
{times.map((time) => (
|
||||
<SelectOption key={time.label} value={time.multiplier}>
|
||||
{time.label}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
);
|
||||
};
|
|
@ -19,12 +19,14 @@ export const useAccess = () => useContext(AccessContext);
|
|||
type AccessProviderProps = { children: React.ReactNode };
|
||||
export const AccessContextProvider = ({ children }: AccessProviderProps) => {
|
||||
const { whoAmI } = useContext(WhoAmIContext);
|
||||
const realmCtx = useContext(RealmContext);
|
||||
const { realm } = useContext(RealmContext);
|
||||
const [access, setAccess] = useState<readonly AccessType[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setAccess(whoAmI.getRealmAccess()[realmCtx.realm]);
|
||||
}, [whoAmI]);
|
||||
if (whoAmI.getRealmAccess()[realm]) {
|
||||
setAccess(whoAmI.getRealmAccess()[realm]);
|
||||
}
|
||||
}, [whoAmI, realm]);
|
||||
|
||||
const hasAccess = (...types: AccessType[]) => {
|
||||
return types.every((type) => type === "anyone" || access.includes(type));
|
||||
|
|
|
@ -16,13 +16,18 @@ export const Api = () => (
|
|||
onCloseAlert={() => {}}
|
||||
/>
|
||||
);
|
||||
export const AddAlert = () => {
|
||||
|
||||
const AlertButton = () => {
|
||||
const { addAlert } = useAlerts();
|
||||
return (
|
||||
<Button onClick={() => addAlert("Hello", AlertVariant.default)}>Add</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export const AddAlert = () => {
|
||||
return (
|
||||
<AlertProvider>
|
||||
<Button onClick={() => addAlert("Hello", AlertVariant.default)}>
|
||||
Add
|
||||
</Button>
|
||||
<AlertButton />
|
||||
</AlertProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -27,7 +27,7 @@ export const loadPosts = () => {
|
|||
|
||||
return (
|
||||
<PostLoader url="https://jsonplaceholder.typicode.com/posts">
|
||||
{(posts: { data: Post[] }) => (
|
||||
{(posts: Post[]) => (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -36,7 +36,7 @@ export const loadPosts = () => {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{posts.data.map((post, i) => (
|
||||
{posts.map((post, i) => (
|
||||
<tr key={i}>
|
||||
<td>{post.title}</td>
|
||||
<td>{post.body}</td>
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import { Meta } from "@storybook/react";
|
||||
|
||||
import serverInfo from "../context/server-info/__tests__/mock.json";
|
||||
import { ServerInfoContext } from "../context/server-info/ServerInfoProvider";
|
||||
|
||||
import {
|
||||
DownloadDialog,
|
||||
useDownloadDialog,
|
||||
} from "../components/download-dialog/DownloadDialog";
|
||||
import { DownloadDialog } from "../components/download-dialog/DownloadDialog";
|
||||
import { MockAdminClient } from "./MockAdminClient";
|
||||
|
||||
export default {
|
||||
|
@ -16,20 +10,22 @@ export default {
|
|||
} as Meta;
|
||||
|
||||
const Test = () => {
|
||||
const [toggle, Dialog] = useDownloadDialog({
|
||||
id: "58577281-7af7-410c-a085-61ff3040be6d",
|
||||
});
|
||||
const [open, setOpen] = useState(false);
|
||||
const toggle = () => setOpen(!open);
|
||||
|
||||
return (
|
||||
<ServerInfoContext.Provider value={serverInfo}>
|
||||
<MockAdminClient
|
||||
mock={{ clients: { getInstallationProviders: () => '{some: "json"}' } }}
|
||||
>
|
||||
<button id="show" onClick={toggle}>
|
||||
Show
|
||||
</button>
|
||||
<Dialog />
|
||||
<DownloadDialog
|
||||
id="58577281-7af7-410c-a085-61ff3040be6d"
|
||||
open={open}
|
||||
toggleDialog={toggle}
|
||||
/>
|
||||
</MockAdminClient>
|
||||
</ServerInfoContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -5,9 +5,11 @@ import KeycloakAdminClient from "keycloak-admin";
|
|||
import { AccessContextProvider } from "../context/access/Access";
|
||||
import { WhoAmIContextProvider } from "../context/whoami/WhoAmI";
|
||||
import { RealmContext } from "../context/realm-context/RealmContext";
|
||||
import { AdminClient } from "../context/auth/AdminClient";
|
||||
import { ServerInfoContext } from "../context/server-info/ServerInfoProvider";
|
||||
|
||||
import whoamiMock from "../context/whoami/__tests__/mock-whoami.json";
|
||||
import { AdminClient } from "../context/auth/AdminClient";
|
||||
import serverInfo from "../context/server-info/__tests__/mock.json";
|
||||
|
||||
/**
|
||||
* This component provides some mocked default react context so that other components can work in a storybook.
|
||||
|
@ -28,12 +30,13 @@ export const MockAdminClient = (props: {
|
|||
}) => {
|
||||
return (
|
||||
<HashRouter>
|
||||
<ServerInfoContext.Provider value={serverInfo}>
|
||||
<AdminClient.Provider
|
||||
value={
|
||||
({
|
||||
...props.mock,
|
||||
keycloak: {},
|
||||
whoAmI: { find: () => whoamiMock },
|
||||
whoAmI: { find: () => Promise.resolve(whoamiMock) },
|
||||
setConfig: () => {},
|
||||
} as unknown) as KeycloakAdminClient
|
||||
}
|
||||
|
@ -46,6 +49,7 @@ export const MockAdminClient = (props: {
|
|||
</RealmContext.Provider>
|
||||
</WhoAmIContextProvider>
|
||||
</AdminClient.Provider>
|
||||
</ServerInfoContext.Provider>
|
||||
</HashRouter>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import { Meta, Story } from "@storybook/react";
|
||||
import { action } from "@storybook/addon-actions";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { Button } from "@patternfly/react-core";
|
||||
|
||||
import {
|
||||
|
@ -16,14 +16,16 @@ export default {
|
|||
} as Meta;
|
||||
|
||||
const Template: Story<MultiLineInputProps> = (args) => {
|
||||
const form = useForm();
|
||||
const form = useForm({ mode: "onChange" });
|
||||
return (
|
||||
<form
|
||||
onSubmit={form.handleSubmit((data) => {
|
||||
action("submit")(toValue(data.items));
|
||||
})}
|
||||
>
|
||||
<MultiLineInput {...args} form={form} />
|
||||
<FormProvider {...form}>
|
||||
<MultiLineInput {...args} />
|
||||
</FormProvider>
|
||||
<br />
|
||||
<br />
|
||||
<Button type="submit">Submit</Button>
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from "react";
|
||||
import { Meta } from "@storybook/react";
|
||||
import { MockAdminClient } from "./MockAdminClient";
|
||||
import { MemoryRouter, Route } from "react-router-dom";
|
||||
import rolesMock from "../realm-roles/__tests__/mock-roles.json";
|
||||
import { RealmRoleTabs } from "../realm-roles/RealmRoleTabs";
|
||||
|
||||
|
@ -13,11 +12,7 @@ export default {
|
|||
export const RolesTabsExample = () => {
|
||||
return (
|
||||
<MockAdminClient mock={{ roles: { findOneById: () => rolesMock[0] } }}>
|
||||
<MemoryRouter initialEntries={["/roles/1"]}>
|
||||
<Route path="/roles/:id">
|
||||
<RealmRoleTabs />
|
||||
</Route>
|
||||
</MemoryRouter>
|
||||
</MockAdminClient>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import { Meta } from "@storybook/react";
|
|||
|
||||
import { RealmSelector } from "../components/realm-selector/RealmSelector";
|
||||
import { RealmContextProvider } from "../context/realm-context/RealmContext";
|
||||
import { HashRouter } from "react-router-dom";
|
||||
|
||||
export default {
|
||||
title: "Header",
|
||||
|
@ -18,6 +19,7 @@ export default {
|
|||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<HashRouter>
|
||||
<RealmContextProvider>
|
||||
<Page
|
||||
sidebar={
|
||||
|
@ -56,5 +58,6 @@ export const Header = () => {
|
|||
}
|
||||
/>
|
||||
</RealmContextProvider>
|
||||
</HashRouter>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -59,14 +59,14 @@ export const convertToFormValues = (
|
|||
setValue: (name: string, value: any) => void
|
||||
) => {
|
||||
return Object.keys(obj).map((key) => {
|
||||
const newKey = key.replace(/\./g, "_");
|
||||
const newKey = key.replace(/\./g, "-");
|
||||
setValue(prefix + "." + newKey, obj[key]);
|
||||
});
|
||||
};
|
||||
|
||||
export const convertFormValuesToObject = (obj: any) => {
|
||||
const keyValues = Object.keys(obj).map((key) => {
|
||||
const newKey = key.replace(/_/g, ".");
|
||||
const newKey = key.replace(/-/g, ".");
|
||||
return { [newKey]: obj[key] };
|
||||
});
|
||||
return Object.assign({}, ...keyValues);
|
||||
|
|
21
yarn.lock
21
yarn.lock
|
@ -10319,11 +10319,16 @@ focus-trap@6.2.2:
|
|||
dependencies:
|
||||
tabbable "^5.1.4"
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.10.0:
|
||||
follow-redirects@^1.0.0:
|
||||
version "1.13.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.1.tgz#5f69b813376cee4fd0474a3aba835df04ab763b7"
|
||||
integrity sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==
|
||||
|
||||
follow-redirects@^1.10.0:
|
||||
version "1.13.2"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.2.tgz#dd73c8effc12728ba5cf4259d760ea5fb83e3147"
|
||||
integrity sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==
|
||||
|
||||
for-in@^0.1.3:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
|
||||
|
@ -13472,10 +13477,10 @@ junk@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
|
||||
integrity sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==
|
||||
|
||||
keycloak-admin@1.14.6:
|
||||
version "1.14.6"
|
||||
resolved "https://registry.yarnpkg.com/keycloak-admin/-/keycloak-admin-1.14.6.tgz#22ae06bc0db7b041914bae6cf8fd5fd20d3efa9b"
|
||||
integrity sha512-bjYwXzq5VRJh/uM/gfogf0SsPn53xb7cMd6R3qp5jY4VrdjnCYPvD1db8F+Cr6lxOxYsA3jVMziMLgGKKuGIdw==
|
||||
keycloak-admin@1.14.7:
|
||||
version "1.14.7"
|
||||
resolved "https://registry.yarnpkg.com/keycloak-admin/-/keycloak-admin-1.14.7.tgz#fe86f296cc6774ec3256b3211d5cd3c76cf79b9c"
|
||||
integrity sha512-PvDcs8E9VVlhe/1Te/TA5AP8gOkQqIxOmI08Eae3gOvxtt7Ucdd4hxa/ZUX5B7/PvOQH+mFqP5XHVovAZWtmFA==
|
||||
dependencies:
|
||||
axios "^0.21.0"
|
||||
camelize "^1.0.0"
|
||||
|
@ -16657,9 +16662,9 @@ query-string@^4.1.0:
|
|||
strict-uri-encode "^1.0.0"
|
||||
|
||||
query-string@^6.13.7:
|
||||
version "6.13.7"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.7.tgz#af53802ff6ed56f3345f92d40a056f93681026ee"
|
||||
integrity sha512-CsGs8ZYb39zu0WLkeOhe0NMePqgYdAuCqxOYKDR5LVCytDZYMGx3Bb+xypvQvPHVPijRXB0HZNFllCzHRe4gEA==
|
||||
version "6.13.8"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.13.8.tgz#8cf231759c85484da3cf05a851810d8e825c1159"
|
||||
integrity sha512-jxJzQI2edQPE/NPUOusNjO/ZOGqr1o2OBa/3M00fU76FsLXDVbJDv/p7ng5OdQyorKrkRz1oqfwmbe5MAMePQg==
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
split-on-first "^1.0.0"
|
||||
|
|
Loading…
Reference in a new issue