Add client registration page (#4250)
This commit is contained in:
parent
989d35fe0e
commit
e65a1effda
15 changed files with 712 additions and 2 deletions
|
@ -0,0 +1,64 @@
|
|||
import ListingPage from "../support/pages/admin-ui/ListingPage";
|
||||
import { ClientRegistrationPage } from "../support/pages/admin-ui/manage/clients/ClientRegistrationPage";
|
||||
import Masthead from "../support/pages/admin-ui/Masthead";
|
||||
import SidebarPage from "../support/pages/admin-ui/SidebarPage";
|
||||
import LoginPage from "../support/pages/LoginPage";
|
||||
import { keycloakBefore } from "../support/util/keycloak_hooks";
|
||||
|
||||
describe("Client registration policies subtab", () => {
|
||||
const loginPage = new LoginPage();
|
||||
const listingPage = new ListingPage();
|
||||
const masthead = new Masthead();
|
||||
const sidebarPage = new SidebarPage();
|
||||
const clientRegistrationPage = new ClientRegistrationPage();
|
||||
|
||||
before(() => {
|
||||
keycloakBefore();
|
||||
loginPage.logIn();
|
||||
sidebarPage.goToClients();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
clientRegistrationPage.goToClientRegistrationTab();
|
||||
sidebarPage.waitForPageLoad();
|
||||
});
|
||||
|
||||
it("add anonymous client registration policy", () => {
|
||||
clientRegistrationPage
|
||||
.createPolicy()
|
||||
.selectRow("max-clients")
|
||||
.fillPolicyForm({
|
||||
name: "new policy",
|
||||
})
|
||||
.formUtils()
|
||||
.save();
|
||||
|
||||
masthead.checkNotificationMessage("New client policy created successfully");
|
||||
clientRegistrationPage.formUtils().cancel();
|
||||
listingPage.itemExist("new policy");
|
||||
});
|
||||
|
||||
it("edit anonymous client registration policy", () => {
|
||||
listingPage.goToItemDetails("new policy");
|
||||
clientRegistrationPage
|
||||
.fillPolicyForm({
|
||||
name: "policy 2",
|
||||
})
|
||||
.formUtils()
|
||||
.save();
|
||||
|
||||
masthead.checkNotificationMessage("Client policy updated successfully");
|
||||
clientRegistrationPage.formUtils().cancel();
|
||||
listingPage.itemExist("policy 2");
|
||||
});
|
||||
|
||||
it("delete anonymous client registration policy", () => {
|
||||
listingPage.clickRowDetails("policy 2").clickDetailMenu("Delete");
|
||||
clientRegistrationPage.modalUtils().confirmModal();
|
||||
|
||||
masthead.checkNotificationMessage(
|
||||
"Client registration policy deleted successfully"
|
||||
);
|
||||
listingPage.itemExist("policy 2", false);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import CommonPage from "../../../CommonPage";
|
||||
|
||||
export class ClientRegistrationPage extends CommonPage {
|
||||
goToClientRegistrationTab() {
|
||||
this.tabUtils().clickTab("registration");
|
||||
return this;
|
||||
}
|
||||
|
||||
createPolicy() {
|
||||
cy.findAllByTestId("createPolicy").click();
|
||||
return this;
|
||||
}
|
||||
|
||||
selectRow(name: string) {
|
||||
cy.findAllByTestId(name).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
fillPolicyForm(props: { name: string }) {
|
||||
cy.findAllByTestId("name").clear().type(props.name);
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -46,12 +46,15 @@
|
|||
"clientSignature": "Will the client sign their saml requests and responses? And should they be validated?",
|
||||
"downloadType": "this is information about the download type",
|
||||
"details": "this is information about the details",
|
||||
"clientPolicyName": "Display name of the policy",
|
||||
"createToken": "An initial access token can only be used to create clients",
|
||||
"expiration": "Specifies how long the token should be valid",
|
||||
"count": "Specifies how many clients can be created using the token",
|
||||
"client-authenticator-type": "Client Authenticator used for authentication of this client against Keycloak server",
|
||||
"registration-access-token": "The registration access token provides access for clients to the client registration service.",
|
||||
"signature-algorithm": "JWA algorithm, which the client needs to use when signing a JWT for authentication. If left blank, the client is allowed to use any algorithm.",
|
||||
"anonymousAccessPolicies": "Those Policies are used when the Client Registration Service is invoked by unauthenticated request. This means that the request does not contain Initial Access Token nor Bearer Token.",
|
||||
"authenticatedAccessPolicies": "Those Policies are used when Client Registration Service is invoked by authenticated request. This means that the request contains Initial Access Token or Bearer Token.",
|
||||
"allowRegexComparison": "If OFF, then the Subject DN from given client certificate must exactly match the given DN from the 'Subject DN' property as described in the RFC8705 specification. The Subject DN can be in the RFC2553 or RFC1779 format. If ON, then the Subject DN from given client certificate should match regex specified by 'Subject DN' property.",
|
||||
"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",
|
||||
|
|
|
@ -313,6 +313,20 @@
|
|||
"copySuccess": "Successfully copied to clipboard!",
|
||||
"clipboardCopyError": "Error copying to clipboard.",
|
||||
"copyToClipboard": "Copy to clipboard",
|
||||
"clientRegistration": "Client registration",
|
||||
"anonymousAccessPolicies": "Anonymous access polices",
|
||||
"authenticatedAccessPolicies": "Authenticated access polices",
|
||||
"provider": "Provider",
|
||||
"providerId": "Provider ID",
|
||||
"providerCreateSuccess": "New client policy created successfully",
|
||||
"providerCreateError": "Could not create client policy due to {{error}}",
|
||||
"providerUpdatedSuccess": "Client policy updated successfully",
|
||||
"providerUpdatedError": "Could not update client policy due to {{error}}",
|
||||
"clientRegisterPolicyDeleteConfirmTitle": "Delete client registration policy?",
|
||||
"clientRegisterPolicyDeleteConfirm": "Are you sure you want to permanently delete the client registration policy {{name}}",
|
||||
"clientRegisterPolicyDeleteSuccess": "Client registration policy deleted successfully",
|
||||
"clientRegisterPolicyDeleteError": "Could not delete client registration policy: '{{error}}'",
|
||||
"chooseAPolicyProvider": "Choose a policy provider",
|
||||
"clientAuthentication": "Client authentication",
|
||||
"authentication": "Authentication",
|
||||
"authenticationFlow": "Authentication flow",
|
||||
|
|
|
@ -156,5 +156,33 @@
|
|||
"client-updater-source-roles": {
|
||||
"label": "Updating entity role",
|
||||
"tooltip": "The condition is checked during client registration/update requests and it evaluates to true if the entity (usually user), who is creating/updating client is member of the specified role. For reference the realm role, you can use the realm role name like 'my_realm_role' . For reference client role, you can use the client_id.role_name for example 'my_client.my_client_role' will refer to client role 'my_client_role' of client 'my_client'."
|
||||
},
|
||||
"allowed-client-scopes": {
|
||||
"label": "Allowed Client Scopes",
|
||||
"tooltip": "Whitelist of the client scopes, which can be used on a newly registered client. Attempt to register client with some client scope, which is not whitelisted, will be rejected. By default, the whitelist is either empty or contains just realm default client scopes (based on 'Allow Default Scopes' configuration property)"
|
||||
},
|
||||
"allow-default-scopes": {
|
||||
"label": "Allow Default Scopes",
|
||||
"tooltip": "If on, newly registered clients will be allowed to have client scopes mentioned in realm default client scopes or realm optional client scopes"
|
||||
},
|
||||
"allowed-protocol-mappers": {
|
||||
"label": "Allowed Protocol Mappers",
|
||||
"tooltip": "Whitelist of allowed protocol mapper providers. If there is an attempt to register client, which contains some protocol mappers, which were not whitelisted, registration request will be rejected."
|
||||
},
|
||||
"max-clients": {
|
||||
"label": "Max Clients Per Realm",
|
||||
"tooltip": "It will not be allowed to register a new client if count of existing clients in realm is same or bigger than the configured limit."
|
||||
},
|
||||
"trusted-hosts": {
|
||||
"label": "Trusted Hosts",
|
||||
"tooltip": "List of Hosts, which are trusted and are allowed to invoke Client Registration Service and/or be used as values of Client URIs. You can use hostnames or IP addresses. If you use star at the beginning (for example '*.example.com' ) then whole domain example.com will be trusted."
|
||||
},
|
||||
"host-sending-registration-request-must-match": {
|
||||
"label": "Host Sending Client Registration Request Must Match",
|
||||
"tooltip": "If on, any request to Client Registration Service is allowed just if it was sent from some trusted host or domain."
|
||||
},
|
||||
"client-uris-must-match": {
|
||||
"label": "Client URIs Must Match",
|
||||
"tooltip": "If on, all Client URIs (Redirect URIs and others) are allowed just if they match some trusted host or domain."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ import {
|
|||
useRoutableTab,
|
||||
} from "../components/routable-tabs/RoutableTabs";
|
||||
import { ClientsTab, toClients } from "./routes/Clients";
|
||||
import { ClientRegistration } from "./registration/ClientRegistration";
|
||||
|
||||
export default function ClientsSection() {
|
||||
const { t } = useTranslation("clients");
|
||||
|
@ -69,6 +70,7 @@ export default function ClientsSection() {
|
|||
|
||||
const listTab = useTab("list");
|
||||
const initialAccessTokenTab = useTab("initial-access-token");
|
||||
const clientRegistrationTab = useTab("client-registration");
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: t("clientDelete", { clientId: selectedClient?.clientId }),
|
||||
|
@ -243,6 +245,13 @@ export default function ClientsSection() {
|
|||
>
|
||||
<InitialAccessTokenList />
|
||||
</Tab>
|
||||
<Tab
|
||||
data-testid="registration"
|
||||
title={<TabTitleText>{t("clientRegistration")}</TabTitleText>}
|
||||
{...clientRegistrationTab}
|
||||
>
|
||||
<ClientRegistration />
|
||||
</Tab>
|
||||
</RoutableTabs>
|
||||
</PageSection>
|
||||
</>
|
||||
|
|
|
@ -47,7 +47,7 @@ export const InitialAccessTokenList = () => {
|
|||
addAlert(t("tokenDeleteSuccess"), AlertVariant.success);
|
||||
setToken(undefined);
|
||||
} catch (error) {
|
||||
addError("tokenDeleteError", error);
|
||||
addError("clients:tokenDeleteError", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
96
apps/admin-ui/src/clients/registration/AddProviderDialog.tsx
Normal file
96
apps/admin-ui/src/clients/registration/AddProviderDialog.tsx
Normal file
|
@ -0,0 +1,96 @@
|
|||
import {
|
||||
DataList,
|
||||
DataListCell,
|
||||
DataListItem,
|
||||
DataListItemCells,
|
||||
DataListItemRow,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
} from "@patternfly/react-core";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||
import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort";
|
||||
|
||||
type AddProviderDialogProps = {
|
||||
onConfirm: (providerId: string) => void;
|
||||
toggleDialog: () => void;
|
||||
};
|
||||
|
||||
export const AddProviderDialog = ({
|
||||
onConfirm,
|
||||
toggleDialog,
|
||||
}: AddProviderDialogProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const serverInfo = useServerInfo();
|
||||
const providers = Object.keys(
|
||||
serverInfo.providers?.["client-registration-policy"].providers || []
|
||||
);
|
||||
|
||||
const descriptions =
|
||||
serverInfo.componentTypes?.[
|
||||
"org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy"
|
||||
];
|
||||
const localeSort = useLocaleSort();
|
||||
|
||||
const rows = useMemo(
|
||||
() =>
|
||||
localeSort(
|
||||
descriptions?.filter((d) => providers.includes(d.id)) || [],
|
||||
mapByKey("id")
|
||||
),
|
||||
[providers, descriptions]
|
||||
);
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.medium}
|
||||
title={t("chooseAPolicyProvider")}
|
||||
isOpen
|
||||
onClose={toggleDialog}
|
||||
>
|
||||
<DataList
|
||||
onSelectDataListItem={(id) => {
|
||||
onConfirm(id);
|
||||
toggleDialog();
|
||||
}}
|
||||
aria-label={t("addPredefinedMappers")}
|
||||
isCompact
|
||||
>
|
||||
<DataListItem aria-label={t("headerName")} id="header">
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[t("common:name"), t("common:description")].map(
|
||||
(name) => (
|
||||
<DataListCell style={{ fontWeight: 700 }} key={name}>
|
||||
{name}
|
||||
</DataListCell>
|
||||
)
|
||||
)}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
{rows.map((provider) => (
|
||||
<DataListItem
|
||||
aria-label={provider.id}
|
||||
key={provider.id}
|
||||
data-testid={provider.id}
|
||||
id={provider.id}
|
||||
>
|
||||
<DataListItemRow>
|
||||
<DataListItemCells
|
||||
dataListCells={[
|
||||
<DataListCell width={2} key={`name-${provider.id}`}>
|
||||
{provider.id}
|
||||
</DataListCell>,
|
||||
<DataListCell width={4} key={`description-${provider.id}`}>
|
||||
{provider.helpText}
|
||||
</DataListCell>,
|
||||
]}
|
||||
/>
|
||||
</DataListItemRow>
|
||||
</DataListItem>
|
||||
))}
|
||||
</DataList>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,66 @@
|
|||
import { Tab, TabTitleText } from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import {
|
||||
RoutableTabs,
|
||||
useRoutableTab,
|
||||
} from "../../components/routable-tabs/RoutableTabs";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import {
|
||||
ClientRegistrationTab,
|
||||
toClientRegistration,
|
||||
} from "../routes/ClientRegistration";
|
||||
import { ClientRegistrationList } from "./ClientRegistrationList";
|
||||
|
||||
export const ClientRegistration = () => {
|
||||
const { t } = useTranslation("clients");
|
||||
const { realm } = useRealm();
|
||||
|
||||
const useTab = (subTab: ClientRegistrationTab) =>
|
||||
useRoutableTab(toClientRegistration({ realm, subTab }));
|
||||
|
||||
const anonymousTab = useTab("anonymous");
|
||||
const authenticatedTab = useTab("authenticated");
|
||||
|
||||
return (
|
||||
<RoutableTabs
|
||||
defaultLocation={toClientRegistration({ realm, subTab: "anonymous" })}
|
||||
mountOnEnter
|
||||
>
|
||||
<Tab
|
||||
data-testid="anonymous"
|
||||
title={
|
||||
<TabTitleText>
|
||||
{t("anonymousAccessPolicies")}{" "}
|
||||
<HelpItem
|
||||
fieldLabelId=""
|
||||
helpText="clients-help:anonymousAccessPolicies"
|
||||
noVerticalAlign={false}
|
||||
unWrap
|
||||
/>
|
||||
</TabTitleText>
|
||||
}
|
||||
{...anonymousTab}
|
||||
>
|
||||
<ClientRegistrationList subType="anonymous" />
|
||||
</Tab>
|
||||
<Tab
|
||||
data-testid="authenticated"
|
||||
title={
|
||||
<TabTitleText>
|
||||
{t("authenticatedAccessPolicies")}{" "}
|
||||
<HelpItem
|
||||
fieldLabelId=""
|
||||
helpText="clients-help:authenticatedAccessPolicies"
|
||||
noVerticalAlign={false}
|
||||
unWrap
|
||||
/>
|
||||
</TabTitleText>
|
||||
}
|
||||
{...authenticatedTab}
|
||||
>
|
||||
<ClientRegistrationList subType="authenticated" />
|
||||
</Tab>
|
||||
</RoutableTabs>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,132 @@
|
|||
import ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation";
|
||||
import { Button, ButtonVariant, ToolbarItem } from "@patternfly/react-core";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import useToggle from "../../utils/useToggle";
|
||||
|
||||
import { toRegistrationProvider } from "../routes/AddRegistrationProvider";
|
||||
import { ClientRegistrationParams } from "../routes/ClientRegistration";
|
||||
import { AddProviderDialog } from "./AddProviderDialog";
|
||||
|
||||
type ClientRegistrationListProps = {
|
||||
subType: "anonymous" | "authenticated";
|
||||
};
|
||||
|
||||
export const ClientRegistrationList = ({
|
||||
subType,
|
||||
}: ClientRegistrationListProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const { subTab } = useParams<ClientRegistrationParams>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { adminClient } = useAdminClient();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const { realm } = useRealm();
|
||||
const [policies, setPolicies] = useState<ComponentRepresentation[]>([]);
|
||||
const [selectedPolicy, setSelectedPolicy] =
|
||||
useState<ComponentRepresentation>();
|
||||
const [isAddDialogOpen, toggleAddDialog] = useToggle();
|
||||
|
||||
useFetch(
|
||||
() =>
|
||||
adminClient.components.find({
|
||||
type: "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy",
|
||||
}),
|
||||
(policies) => setPolicies(policies.filter((p) => p.subType === subType)),
|
||||
[selectedPolicy]
|
||||
);
|
||||
|
||||
const DetailLink = (comp: ComponentRepresentation) => (
|
||||
<Link
|
||||
key={comp.id}
|
||||
to={toRegistrationProvider({
|
||||
realm,
|
||||
subTab: subTab || "anonymous",
|
||||
providerId: comp.providerId!,
|
||||
id: comp.id,
|
||||
})}
|
||||
>
|
||||
{comp.name}
|
||||
</Link>
|
||||
);
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: "clients:clientRegisterPolicyDeleteConfirmTitle",
|
||||
messageKey: t("clientRegisterPolicyDeleteConfirm", {
|
||||
name: selectedPolicy?.name,
|
||||
}),
|
||||
continueButtonLabel: "common:delete",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await adminClient.components.del({
|
||||
realm,
|
||||
id: selectedPolicy?.id!,
|
||||
});
|
||||
addAlert(t("clientRegisterPolicyDeleteSuccess"));
|
||||
setSelectedPolicy(undefined);
|
||||
} catch (error) {
|
||||
addError("clients:clientRegisterPolicyDeleteError", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAddDialogOpen && (
|
||||
<AddProviderDialog
|
||||
onConfirm={(providerId) =>
|
||||
navigate(
|
||||
toRegistrationProvider({
|
||||
realm,
|
||||
subTab: subTab || "anonymous",
|
||||
providerId,
|
||||
})
|
||||
)
|
||||
}
|
||||
toggleDialog={toggleAddDialog}
|
||||
/>
|
||||
)}
|
||||
<DeleteConfirm />
|
||||
<KeycloakDataTable
|
||||
ariaLabelKey="clients:initialAccessToken"
|
||||
searchPlaceholderKey="clients:searchInitialAccessToken"
|
||||
loader={policies}
|
||||
toolbarItem={
|
||||
<ToolbarItem>
|
||||
<Button data-testid="createPolicy" onClick={toggleAddDialog}>
|
||||
{t("createPolicy")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
title: t("common:delete"),
|
||||
onRowClick: (policy) => {
|
||||
setSelectedPolicy(policy);
|
||||
toggleDeleteDialog();
|
||||
},
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
name: "name",
|
||||
displayKey: "common:name",
|
||||
cellRenderer: DetailLink,
|
||||
},
|
||||
{
|
||||
name: "providerId",
|
||||
displayKey: "clients:providerId",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
204
apps/admin-ui/src/clients/registration/DetailProvider.tsx
Normal file
204
apps/admin-ui/src/clients/registration/DetailProvider.tsx
Normal file
|
@ -0,0 +1,204 @@
|
|||
import ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentRepresentation";
|
||||
import ComponentTypeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentTypeRepresentation";
|
||||
import {
|
||||
ActionGroup,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
DropdownItem,
|
||||
FormGroup,
|
||||
PageSection,
|
||||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
import { useState } from "react";
|
||||
import { FormProvider, useForm, useWatch } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||
import { DynamicComponents } from "../../components/dynamic/DynamicComponents";
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
|
||||
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
|
||||
import { ViewHeader } from "../../components/view-header/ViewHeader";
|
||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { useParams } from "../../utils/useParams";
|
||||
import {
|
||||
RegistrationProviderParams,
|
||||
toRegistrationProvider,
|
||||
} from "../routes/AddRegistrationProvider";
|
||||
import { toClientRegistration } from "../routes/ClientRegistration";
|
||||
|
||||
export default function DetailProvider() {
|
||||
const { t } = useTranslation("clients");
|
||||
const { id, providerId, subTab } = useParams<RegistrationProviderParams>();
|
||||
const navigate = useNavigate();
|
||||
const form = useForm<ComponentRepresentation>({
|
||||
defaultValues: { providerId },
|
||||
});
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = form;
|
||||
|
||||
const { adminClient } = useAdminClient();
|
||||
const { realm } = useRealm();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
const [provider, setProvider] = useState<ComponentTypeRepresentation>();
|
||||
const [parentId, setParentId] = useState("");
|
||||
|
||||
useFetch(
|
||||
async () =>
|
||||
await Promise.all([
|
||||
adminClient.realms.getClientRegistrationPolicyProviders({ realm }),
|
||||
adminClient.realms.findOne({ realm }),
|
||||
id ? adminClient.components.findOne({ id }) : Promise.resolve(),
|
||||
]),
|
||||
([providers, realm, data]) => {
|
||||
setProvider(providers.find((p) => p.id === providerId));
|
||||
setParentId(realm?.id || "");
|
||||
reset(data || { providerId });
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const providerName = useWatch({ control, defaultValue: "", name: "name" });
|
||||
|
||||
const onSubmit = async (component: ComponentRepresentation) => {
|
||||
if (component.config)
|
||||
Object.entries(component.config).forEach(
|
||||
([key, value]) =>
|
||||
(component.config![key] = Array.isArray(value) ? value : [value])
|
||||
);
|
||||
try {
|
||||
const updatedComponent = {
|
||||
...component,
|
||||
subType: subTab,
|
||||
parentId,
|
||||
providerType:
|
||||
"org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy",
|
||||
providerId,
|
||||
};
|
||||
if (id) {
|
||||
await adminClient.components.update({ id }, updatedComponent);
|
||||
} else {
|
||||
const { id } = await adminClient.components.create(updatedComponent);
|
||||
navigate(toRegistrationProvider({ id, realm, subTab, providerId }));
|
||||
}
|
||||
addAlert(t(`provider${id ? "Updated" : "Create"}Success`));
|
||||
} catch (error) {
|
||||
addError(`clients:provider${id ? "Updated" : "Create"}Error`, error);
|
||||
}
|
||||
};
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: "clients:clientRegisterPolicyDeleteConfirmTitle",
|
||||
messageKey: t("clientRegisterPolicyDeleteConfirm", {
|
||||
name: providerName,
|
||||
}),
|
||||
continueButtonLabel: "common:delete",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await adminClient.components.del({
|
||||
realm,
|
||||
id: id!,
|
||||
});
|
||||
addAlert(t("clientRegisterPolicyDeleteSuccess"));
|
||||
navigate(toClientRegistration({ realm, subTab }));
|
||||
} catch (error) {
|
||||
addError("clients:clientRegisterPolicyDeleteError", error);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (!provider) {
|
||||
return <KeycloakSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader
|
||||
titleKey={id ? providerName! : "clients:createPolicy"}
|
||||
subKey={id}
|
||||
dropdownItems={
|
||||
id
|
||||
? [
|
||||
<DropdownItem
|
||||
data-testid="delete"
|
||||
key="delete"
|
||||
onClick={toggleDeleteDialog}
|
||||
>
|
||||
{t("common:delete")}
|
||||
</DropdownItem>,
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<DeleteConfirm />
|
||||
<PageSection variant="light">
|
||||
<FormAccess
|
||||
role="manage-clients"
|
||||
isHorizontal
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
>
|
||||
<FormGroup label={t("provider")} fieldId="provider">
|
||||
<KeycloakTextInput
|
||||
id="providerId"
|
||||
data-testid="providerId"
|
||||
{...register("providerId")}
|
||||
readOnly
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("common:name")}
|
||||
fieldId="kc-name"
|
||||
helperTextInvalid={t("common:required")}
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:clientPolicyName"
|
||||
fieldLabelId="kc-name"
|
||||
/>
|
||||
}
|
||||
isRequired
|
||||
>
|
||||
<KeycloakTextInput
|
||||
id="kc-name"
|
||||
data-testid="name"
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
{...register("name", { required: true })}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormProvider {...form}>
|
||||
<DynamicComponents properties={provider.properties} />
|
||||
</FormProvider>
|
||||
<ActionGroup>
|
||||
<Button data-testid="save" type="submit">
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
component={(props) => (
|
||||
<Link
|
||||
{...props}
|
||||
to={toClientRegistration({ realm, subTab })}
|
||||
></Link>
|
||||
)}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,7 +1,12 @@
|
|||
import type { RouteDef } from "../route-config";
|
||||
import { AddClientRoute } from "./routes/AddClient";
|
||||
import {
|
||||
AddRegistrationProviderRoute,
|
||||
EditRegistrationProviderRoute,
|
||||
} from "./routes/AddRegistrationProvider";
|
||||
import { AuthorizationRoute } from "./routes/AuthenticationTab";
|
||||
import { ClientRoute } from "./routes/Client";
|
||||
import { ClientRegistrationRoute } from "./routes/ClientRegistration";
|
||||
import { ClientRoleRoute } from "./routes/ClientRole";
|
||||
import { ClientsRoute, ClientsRouteWithTab } from "./routes/Clients";
|
||||
import { ClientScopesRoute } from "./routes/ClientScopeTab";
|
||||
|
@ -32,6 +37,9 @@ import {
|
|||
} from "./routes/Scope";
|
||||
|
||||
const routes: RouteDef[] = [
|
||||
ClientRegistrationRoute,
|
||||
AddRegistrationProviderRoute,
|
||||
EditRegistrationProviderRoute,
|
||||
AddClientRoute,
|
||||
ImportClientRoute,
|
||||
ClientsRoute,
|
||||
|
|
36
apps/admin-ui/src/clients/routes/AddRegistrationProvider.ts
Normal file
36
apps/admin-ui/src/clients/routes/AddRegistrationProvider.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { lazy } from "react";
|
||||
import type { Path } from "react-router-dom";
|
||||
import { generatePath } from "react-router-dom";
|
||||
import type { RouteDef } from "../../route-config";
|
||||
import { ClientRegistrationTab } from "./ClientRegistration";
|
||||
|
||||
export type RegistrationProviderParams = {
|
||||
realm: string;
|
||||
subTab: ClientRegistrationTab;
|
||||
id?: string;
|
||||
providerId: string;
|
||||
};
|
||||
|
||||
export const AddRegistrationProviderRoute: RouteDef = {
|
||||
path: "/:realm/clients/client-registration/:subTab/:providerId",
|
||||
component: lazy(() => import("../registration/DetailProvider")),
|
||||
breadcrumb: (t) => t("clients:clientSettings"),
|
||||
access: "manage-clients",
|
||||
};
|
||||
|
||||
export const EditRegistrationProviderRoute: RouteDef = {
|
||||
...AddRegistrationProviderRoute,
|
||||
path: "/:realm/clients/client-registration/:subTab/:providerId/:id",
|
||||
};
|
||||
|
||||
export const toRegistrationProvider = (
|
||||
params: RegistrationProviderParams
|
||||
): Partial<Path> => {
|
||||
const path = params.id
|
||||
? EditRegistrationProviderRoute.path
|
||||
: AddRegistrationProviderRoute.path;
|
||||
|
||||
return {
|
||||
pathname: generatePath(path, params),
|
||||
};
|
||||
};
|
24
apps/admin-ui/src/clients/routes/ClientRegistration.ts
Normal file
24
apps/admin-ui/src/clients/routes/ClientRegistration.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { lazy } from "react";
|
||||
import type { Path } from "react-router-dom";
|
||||
import { generatePath } from "react-router-dom";
|
||||
import type { RouteDef } from "../../route-config";
|
||||
|
||||
export type ClientRegistrationTab = "anonymous" | "authenticated";
|
||||
|
||||
export type ClientRegistrationParams = {
|
||||
realm: string;
|
||||
subTab: ClientRegistrationTab;
|
||||
};
|
||||
|
||||
export const ClientRegistrationRoute: RouteDef = {
|
||||
path: "/:realm/clients/client-registration/:subTab",
|
||||
component: lazy(() => import("../ClientsSection")),
|
||||
breadcrumb: (t) => t("clients:clientRegistration"),
|
||||
access: "view-clients",
|
||||
};
|
||||
|
||||
export const toClientRegistration = (
|
||||
params: ClientRegistrationParams
|
||||
): Partial<Path> => ({
|
||||
pathname: generatePath(ClientRegistrationRoute.path, params),
|
||||
});
|
|
@ -3,7 +3,10 @@ import type { Path } from "react-router-dom";
|
|||
import { generatePath } from "react-router-dom";
|
||||
import type { RouteDef } from "../../route-config";
|
||||
|
||||
export type ClientsTab = "list" | "initial-access-token";
|
||||
export type ClientsTab =
|
||||
| "list"
|
||||
| "initial-access-token"
|
||||
| "client-registration";
|
||||
|
||||
export type ClientsParams = {
|
||||
realm: string;
|
||||
|
|
Loading…
Reference in a new issue