Profile view update (#1357)
* added routing for viewing client profile * added add executors form template * added add executors form template * add executor: wip * add executor: wip * add executor: wip * add executor: wip * add executor: wip * add executor: wip * add executor: wip * add executor: wip * add executor: wip * added adding excutors to profiles * added displaying executors - wip * added displaying executors - wip * added navigation to client profile edit on executor creation * replaced table with list for listing executors added * added support for editing client profile * added logic making executors with config links only * added read only values for edit/view client temporarily * added helpText for added executors listed in executors list * added helpText for added executors listed in executors list * added deleting executor from client profile * fixed deleting client profile and fixed messages for delete modals * fixed message for delete clinet profile modal * combined delete dialogs for client profile and executor * displaying global executors for global profiles, hiding add executor button * fixed eslint issue * fixed executors list * added back button to global profile view/edit view * fixed test * added global batche and hid actions dropdown for global profiles * fixed switch on/off labels * fixed hide/display items for global and non-global profiles * added isDirty * feedback fixes * feedback fixes * feedback fixes * feedback fixes * feedback fixes * feedback fix * small refactor * added name and removed unused state * fixed executor creation * fixed executor creation * added saving edited client profile * added saving edited client profile * improved trash icon styles * test fix * Some code suggestions * Some more code suggestions * feedback fixes * feedback fixes * use find instead of filter * feedback fixes * added defaultValues for executors * removed defaultValues for executors * final feedback fixes * minor fixes Co-authored-by: Agnieszka Gancarczyk <agancarc@redhat.com> Co-authored-by: Erik Jan de Wit <erikjan.dewit@gmail.com> Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
aad3b2ba43
commit
56eb774dd3
24 changed files with 1009 additions and 371 deletions
|
@ -437,6 +437,7 @@ describe("Realm settings tests", () => {
|
||||||
9
|
9
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Realm settings client profiles tab tests", () => {
|
describe("Realm settings client profiles tab tests", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -491,8 +492,9 @@ describe("Realm settings tests", () => {
|
||||||
realmSettingsPage.shouldCompleteAndCreateNewClientProfile();
|
realmSettingsPage.shouldCompleteAndCreateNewClientProfile();
|
||||||
realmSettingsPage.shouldNotCreateDuplicateClientProfile();
|
realmSettingsPage.shouldNotCreateDuplicateClientProfile();
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Realm settings client policies tab tests", () => {
|
describe.skip("Realm settings client policies tab tests", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
keycloakBefore();
|
keycloakBefore();
|
||||||
loginPage.logIn();
|
loginPage.logIn();
|
||||||
|
@ -562,5 +564,3 @@ describe("Realm settings tests", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
|
@ -709,11 +709,11 @@ export default class RealmSettingsPage {
|
||||||
cy.get(this.moreDrpDwnItems).click();
|
cy.get(this.moreDrpDwnItems).click();
|
||||||
cy.get(this.deleteDialogTitle).contains("Delete profile?");
|
cy.get(this.deleteDialogTitle).contains("Delete profile?");
|
||||||
cy.get(this.deleteDialogBodyText).contains(
|
cy.get(this.deleteDialogBodyText).contains(
|
||||||
"This action will permanently delete the profile custom-profile. This cannot be undone."
|
"This action will permanently delete the profile Test. This cannot be undone."
|
||||||
);
|
);
|
||||||
cy.findByTestId("modalConfirm").contains("Delete");
|
cy.findByTestId("modalConfirm").contains("Delete");
|
||||||
cy.get(this.deleteDialogCancelBtn).contains("Cancel").click();
|
cy.get(this.deleteDialogCancelBtn).contains("Cancel").click();
|
||||||
cy.get("table").should("not.have.text", "Test");
|
cy.get("table").should("be.visible").contains("td", "Test");
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldDeleteClientPolicyDialog() {
|
shouldDeleteClientPolicyDialog() {
|
||||||
|
|
|
@ -26,6 +26,7 @@ export const BooleanComponent = ({
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name={`config.${name?.replaceAll(".", "-")}`}
|
name={`config.${name?.replaceAll(".", "-")}`}
|
||||||
|
data-testid={name}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
|
@ -33,7 +34,7 @@ export const BooleanComponent = ({
|
||||||
id={name!}
|
id={name!}
|
||||||
label={t("common:on")}
|
label={t("common:on")}
|
||||||
labelOff={t("common:off")}
|
labelOff={t("common:off")}
|
||||||
isChecked={value === "true"}
|
isChecked={value === "true" || value === true}
|
||||||
onChange={(value) => onChange("" + value)}
|
onChange={(value) => onChange("" + value)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -32,6 +32,7 @@ export const ListComponent = ({
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name={`config.${name?.replaceAll(".", "-")}`}
|
name={`config.${name?.replaceAll(".", "-")}`}
|
||||||
|
data-testid={name}
|
||||||
defaultValue={defaultValue || ""}
|
defaultValue={defaultValue || ""}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import {
|
||||||
|
FormGroup,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
SelectVariant,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
|
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||||
|
import type { ComponentProps } from "./components";
|
||||||
|
|
||||||
|
export const MultivaluedListComponent = ({
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
helpText,
|
||||||
|
defaultValue,
|
||||||
|
options,
|
||||||
|
}: ComponentProps) => {
|
||||||
|
const { t } = useTranslation("client-scopes");
|
||||||
|
const { control } = useFormContext();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
label={t(label!)}
|
||||||
|
labelIcon={
|
||||||
|
<HelpItem helpText={t(helpText!)} forLabel={t(label!)} forID={name!} />
|
||||||
|
}
|
||||||
|
fieldId={name!}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name={label!}
|
||||||
|
defaultValue={defaultValue || []}
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
toggleId={name}
|
||||||
|
data-testid={name}
|
||||||
|
chipGroupProps={{
|
||||||
|
numChips: 1,
|
||||||
|
expandedText: t("common:hide"),
|
||||||
|
collapsedText: t("common:showRemaining"),
|
||||||
|
}}
|
||||||
|
variant={SelectVariant.typeaheadMulti}
|
||||||
|
typeAheadAriaLabel={t("common:select")}
|
||||||
|
onToggle={(isOpen) => setOpen(isOpen)}
|
||||||
|
selections={value}
|
||||||
|
onSelect={(_, v) => {
|
||||||
|
const option = v.toString();
|
||||||
|
if (!value) {
|
||||||
|
onChange([option]);
|
||||||
|
} else if (value.includes(option)) {
|
||||||
|
onChange(value.filter((item: string) => item !== option));
|
||||||
|
} else {
|
||||||
|
onChange([...value, option]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClear={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onChange([]);
|
||||||
|
}}
|
||||||
|
isOpen={open}
|
||||||
|
aria-label={t(label!)}
|
||||||
|
>
|
||||||
|
{options?.map((option) => (
|
||||||
|
<SelectOption key={option} value={option} />
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
);
|
||||||
|
};
|
|
@ -142,6 +142,7 @@ export const RoleComponent = ({ name, label, helpText }: ComponentProps) => {
|
||||||
{clients && (
|
{clients && (
|
||||||
<Select
|
<Select
|
||||||
toggleId={name!}
|
toggleId={name!}
|
||||||
|
data-testid={name}
|
||||||
onToggle={() => setClientsOpen(!clientsOpen)}
|
onToggle={() => setClientsOpen(!clientsOpen)}
|
||||||
isOpen={clientsOpen}
|
isOpen={clientsOpen}
|
||||||
variant={SelectVariant.typeahead}
|
variant={SelectVariant.typeahead}
|
||||||
|
|
|
@ -35,6 +35,7 @@ export const ScriptComponent = ({
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
id={name!}
|
id={name!}
|
||||||
|
data-testid={name}
|
||||||
type="text"
|
type="text"
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
code={value}
|
code={value}
|
||||||
|
|
|
@ -25,6 +25,7 @@ export const StringComponent = ({
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
id={name!}
|
id={name!}
|
||||||
|
data-testid={name}
|
||||||
ref={register()}
|
ref={register()}
|
||||||
type="text"
|
type="text"
|
||||||
name={`config.${name?.replaceAll(".", "-")}`}
|
name={`config.${name?.replaceAll(".", "-")}`}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { BooleanComponent } from "./BooleanComponent";
|
||||||
import { ListComponent } from "./ListComponent";
|
import { ListComponent } from "./ListComponent";
|
||||||
import { RoleComponent } from "./RoleComponent";
|
import { RoleComponent } from "./RoleComponent";
|
||||||
import { ScriptComponent } from "./ScriptComponent";
|
import { ScriptComponent } from "./ScriptComponent";
|
||||||
|
import { MultivaluedListComponent } from "./MultivaluedListComponent";
|
||||||
import { ClientSelectComponent } from "./ClientSelectComponent";
|
import { ClientSelectComponent } from "./ClientSelectComponent";
|
||||||
|
|
||||||
export type ComponentProps = Omit<ConfigPropertyRepresentation, "type">;
|
export type ComponentProps = Omit<ConfigPropertyRepresentation, "type">;
|
||||||
|
@ -15,6 +16,7 @@ const ComponentTypes = [
|
||||||
"List",
|
"List",
|
||||||
"Role",
|
"Role",
|
||||||
"Script",
|
"Script",
|
||||||
|
"MultivaluedList",
|
||||||
"ClientList",
|
"ClientList",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
@ -28,5 +30,9 @@ export const COMPONENTS: {
|
||||||
List: ListComponent,
|
List: ListComponent,
|
||||||
Role: RoleComponent,
|
Role: RoleComponent,
|
||||||
Script: ScriptComponent,
|
Script: ScriptComponent,
|
||||||
|
MultivaluedList: MultivaluedListComponent,
|
||||||
ClientList: ClientSelectComponent,
|
ClientList: ClientSelectComponent,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
export const isValidComponentType = (value: string): value is Components =>
|
||||||
|
value in COMPONENTS;
|
||||||
|
|
|
@ -25,8 +25,8 @@ import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||||
import { convertFormValuesToObject, convertToFormValues } from "../../util";
|
import { convertFormValuesToObject, convertToFormValues } from "../../util";
|
||||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
|
import { COMPONENTS, isValidComponentType } from "../add/components/components";
|
||||||
import { MapperParams, MapperRoute } from "../routes/Mapper";
|
import { MapperParams, MapperRoute } from "../routes/Mapper";
|
||||||
import { Components, COMPONENTS } from "../add/components/components";
|
|
||||||
import { toClientScope } from "../routes/ClientScope";
|
import { toClientScope } from "../routes/ClientScope";
|
||||||
|
|
||||||
import "./mapping-details.css";
|
import "./mapping-details.css";
|
||||||
|
@ -178,9 +178,6 @@ export const MappingDetails = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const isValidComponentType = (value: string): value is Components =>
|
|
||||||
value in COMPONENTS;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteConfirm />
|
<DeleteConfirm />
|
||||||
|
|
|
@ -34,6 +34,7 @@ export default {
|
||||||
disabled: "Disabled",
|
disabled: "Disabled",
|
||||||
disable: "Disable",
|
disable: "Disable",
|
||||||
selectOne: "Select an option",
|
selectOne: "Select an option",
|
||||||
|
select: "Select",
|
||||||
choose: "Choose...",
|
choose: "Choose...",
|
||||||
any: "Any",
|
any: "Any",
|
||||||
none: "None",
|
none: "None",
|
||||||
|
@ -48,6 +49,9 @@ export default {
|
||||||
documentation: "Documentation",
|
documentation: "Documentation",
|
||||||
enableHelpMode: "Enable help mode",
|
enableHelpMode: "Enable help mode",
|
||||||
learnMore: "Learn more",
|
learnMore: "Learn more",
|
||||||
|
show: "Show",
|
||||||
|
hide: "Hide",
|
||||||
|
showRemaining: "Show ${remaining}",
|
||||||
test: "Test",
|
test: "Test",
|
||||||
testConnection: "Test connection",
|
testConnection: "Test connection",
|
||||||
name: "Name",
|
name: "Name",
|
||||||
|
|
|
@ -271,8 +271,8 @@ export const AdminEvents = () => {
|
||||||
data-testid="resource-types-searchField"
|
data-testid="resource-types-searchField"
|
||||||
chipGroupProps={{
|
chipGroupProps={{
|
||||||
numChips: 1,
|
numChips: 1,
|
||||||
expandedText: "Hide",
|
expandedText: t("common:hide"),
|
||||||
collapsedText: "Show ${remaining}",
|
collapsedText: t("common:showRemaining"),
|
||||||
}}
|
}}
|
||||||
variant={SelectVariant.typeaheadMulti}
|
variant={SelectVariant.typeaheadMulti}
|
||||||
typeAheadAriaLabel="Select"
|
typeAheadAriaLabel="Select"
|
||||||
|
@ -336,8 +336,8 @@ export const AdminEvents = () => {
|
||||||
data-testid="operation-types-searchField"
|
data-testid="operation-types-searchField"
|
||||||
chipGroupProps={{
|
chipGroupProps={{
|
||||||
numChips: 1,
|
numChips: 1,
|
||||||
expandedText: "Hide",
|
expandedText: t("common:hide"),
|
||||||
collapsedText: "Show ${remaining}",
|
collapsedText: t("common:showRemaining"),
|
||||||
}}
|
}}
|
||||||
variant={SelectVariant.typeaheadMulti}
|
variant={SelectVariant.typeaheadMulti}
|
||||||
typeAheadAriaLabel="Select"
|
typeAheadAriaLabel="Select"
|
||||||
|
|
|
@ -252,8 +252,8 @@ export const EventsSection = () => {
|
||||||
data-testid="event-type-searchField"
|
data-testid="event-type-searchField"
|
||||||
chipGroupProps={{
|
chipGroupProps={{
|
||||||
numChips: 1,
|
numChips: 1,
|
||||||
expandedText: "Hide",
|
expandedText: t("common:hide"),
|
||||||
collapsedText: "Show ${remaining}",
|
collapsedText: t("common:showRemaining"),
|
||||||
}}
|
}}
|
||||||
variant={SelectVariant.typeaheadMulti}
|
variant={SelectVariant.typeaheadMulti}
|
||||||
typeAheadAriaLabel="Select"
|
typeAheadAriaLabel="Select"
|
||||||
|
|
482
src/realm-settings/ClientProfileForm.tsx
Normal file
482
src/realm-settings/ClientProfileForm.tsx
Normal file
|
@ -0,0 +1,482 @@
|
||||||
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
import {
|
||||||
|
ActionGroup,
|
||||||
|
AlertVariant,
|
||||||
|
Button,
|
||||||
|
ButtonVariant,
|
||||||
|
DataList,
|
||||||
|
DataListCell,
|
||||||
|
DataListItem,
|
||||||
|
DataListItemCells,
|
||||||
|
DataListItemRow,
|
||||||
|
Divider,
|
||||||
|
DropdownItem,
|
||||||
|
Flex,
|
||||||
|
FlexItem,
|
||||||
|
FormGroup,
|
||||||
|
PageSection,
|
||||||
|
Text,
|
||||||
|
TextArea,
|
||||||
|
TextInput,
|
||||||
|
TextVariants,
|
||||||
|
ValidatedOptions,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { FormAccess } from "../components/form-access/FormAccess";
|
||||||
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
|
import { Link, useHistory, useParams } from "react-router-dom";
|
||||||
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
|
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
|
||||||
|
import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
|
||||||
|
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||||
|
import { PlusCircleIcon, TrashIcon } from "@patternfly/react-icons";
|
||||||
|
import "./RealmSettingsSection.css";
|
||||||
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
|
import { toAddExecutor } from "./routes/AddExecutor";
|
||||||
|
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||||
|
import type { ClientProfileParams } from "./routes/ClientProfile";
|
||||||
|
|
||||||
|
type ClientProfileForm = Required<ClientProfileRepresentation>;
|
||||||
|
|
||||||
|
const defaultValues: ClientProfileForm = {
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
executors: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClientProfileForm = () => {
|
||||||
|
const { t } = useTranslation("realm-settings");
|
||||||
|
const history = useHistory();
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
register,
|
||||||
|
errors,
|
||||||
|
formState: { isDirty },
|
||||||
|
} = useForm<ClientProfileForm>({
|
||||||
|
defaultValues,
|
||||||
|
mode: "onChange",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { addAlert, addError } = useAlerts();
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
const [globalProfiles, setGlobalProfiles] = useState<
|
||||||
|
ClientProfileRepresentation[]
|
||||||
|
>([]);
|
||||||
|
const [profiles, setProfiles] = useState<ClientProfileRepresentation[]>([]);
|
||||||
|
const { realm, profileName } = useParams<ClientProfileParams>();
|
||||||
|
const serverInfo = useServerInfo();
|
||||||
|
const executorTypes = useMemo(
|
||||||
|
() =>
|
||||||
|
serverInfo.componentTypes?.[
|
||||||
|
"org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider"
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const [executorToDelete, setExecutorToDelete] =
|
||||||
|
useState<{ idx: number; name: string }>();
|
||||||
|
const editMode = profileName ? true : false;
|
||||||
|
const [key, setKey] = useState(0);
|
||||||
|
const reload = () => setKey(new Date().getTime());
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
() =>
|
||||||
|
adminClient.clientPolicies.listProfiles({ includeGlobalProfiles: true }),
|
||||||
|
(profiles) => {
|
||||||
|
setGlobalProfiles(profiles.globalProfiles ?? []);
|
||||||
|
setProfiles(profiles.profiles ?? []);
|
||||||
|
},
|
||||||
|
[key]
|
||||||
|
);
|
||||||
|
|
||||||
|
const save = async (form: ClientProfileForm) => {
|
||||||
|
const updatedProfiles = editMode ? patchProfiles(form) : addProfile(form);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await adminClient.clientPolicies.createProfiles({
|
||||||
|
profiles: updatedProfiles,
|
||||||
|
globalProfiles: globalProfiles,
|
||||||
|
});
|
||||||
|
|
||||||
|
addAlert(
|
||||||
|
editMode
|
||||||
|
? t("realm-settings:updateClientProfileSuccess")
|
||||||
|
: t("realm-settings:createClientProfileSuccess"),
|
||||||
|
AlertVariant.success
|
||||||
|
);
|
||||||
|
|
||||||
|
history.push(`/${realm}/realm-settings/clientPolicies/${form.name}`);
|
||||||
|
} catch (error) {
|
||||||
|
addError(
|
||||||
|
editMode
|
||||||
|
? "realm-settings:updateClientProfileError"
|
||||||
|
: "realm-settings:createClientProfileError",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const patchProfiles = (data: ClientProfileRepresentation) =>
|
||||||
|
profiles.map((profile) => {
|
||||||
|
if (profile.name !== profileName) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...profile,
|
||||||
|
name: data.name,
|
||||||
|
description: data.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const addProfile = (data: ClientProfileRepresentation) =>
|
||||||
|
profiles.concat({
|
||||||
|
...data,
|
||||||
|
executors: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
|
titleKey: executorToDelete?.name!
|
||||||
|
? t("deleteExecutorProfileConfirmTitle")
|
||||||
|
: t("deleteClientProfileConfirmTitle"),
|
||||||
|
messageKey: executorToDelete?.name!
|
||||||
|
? t("deleteExecutorProfileConfirm", {
|
||||||
|
executorName: executorToDelete.name!,
|
||||||
|
})
|
||||||
|
: t("deleteClientProfileConfirm", {
|
||||||
|
profileName,
|
||||||
|
}),
|
||||||
|
continueButtonLabel: t("delete"),
|
||||||
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
|
|
||||||
|
onConfirm: async () => {
|
||||||
|
if (executorToDelete?.name!) {
|
||||||
|
profileExecutors.splice(executorToDelete.idx!, 1);
|
||||||
|
try {
|
||||||
|
await adminClient.clientPolicies.createProfiles({
|
||||||
|
profiles: profiles,
|
||||||
|
globalProfiles,
|
||||||
|
});
|
||||||
|
addAlert(t("deleteExecutorSuccess"), AlertVariant.success);
|
||||||
|
history.push(
|
||||||
|
`/${realm}/realm-settings/clientPolicies/${profileName}`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
addError(t("deleteExecutorError"), error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const updatedProfiles = profiles.filter(
|
||||||
|
(profile) => profile.name !== profileName
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await adminClient.clientPolicies.createProfiles({
|
||||||
|
profiles: updatedProfiles,
|
||||||
|
globalProfiles,
|
||||||
|
});
|
||||||
|
addAlert(t("deleteClientSuccess"), AlertVariant.success);
|
||||||
|
history.push(`/${realm}/realm-settings/clientPolicies`);
|
||||||
|
} catch (error) {
|
||||||
|
addError(t("deleteClientError"), error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const profile = profiles.find((profile) => profile.name === profileName);
|
||||||
|
const profileExecutors = profile?.executors || [];
|
||||||
|
const globalProfile = globalProfiles.find(
|
||||||
|
(globalProfile) => globalProfile.name === profileName
|
||||||
|
);
|
||||||
|
const globalProfileExecutors = globalProfile?.executors || [];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setValue("name", globalProfile?.name ?? profile?.name);
|
||||||
|
setValue("description", globalProfile?.description ?? profile?.description);
|
||||||
|
}, [profiles]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DeleteConfirm />
|
||||||
|
<ViewHeader
|
||||||
|
titleKey={editMode ? profileName : t("newClientProfile")}
|
||||||
|
badges={[
|
||||||
|
{
|
||||||
|
id: "global-client-profile-badge",
|
||||||
|
text: globalProfile ? t("global") : "",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
divider
|
||||||
|
dropdownItems={
|
||||||
|
!globalProfile
|
||||||
|
? [
|
||||||
|
<DropdownItem
|
||||||
|
key="delete"
|
||||||
|
value="delete"
|
||||||
|
onClick={toggleDeleteDialog}
|
||||||
|
data-testid="deleteClientProfileDropdown"
|
||||||
|
>
|
||||||
|
{t("deleteClientProfile")}
|
||||||
|
</DropdownItem>,
|
||||||
|
]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<PageSection variant="light">
|
||||||
|
<FormAccess isHorizontal role="view-realm" className="pf-u-mt-lg">
|
||||||
|
<FormGroup
|
||||||
|
label={t("newClientProfileName")}
|
||||||
|
fieldId="kc-name"
|
||||||
|
helperText={t("createClientProfileNameHelperText")}
|
||||||
|
isRequired
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
validated={
|
||||||
|
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
ref={register({ required: true })}
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
aria-label={t("name")}
|
||||||
|
data-testid="client-profile-name"
|
||||||
|
isReadOnly={!!globalProfile}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup label={t("common:description")} fieldId="kc-description">
|
||||||
|
<TextArea
|
||||||
|
ref={register()}
|
||||||
|
name="description"
|
||||||
|
type="text"
|
||||||
|
id="description"
|
||||||
|
aria-label={t("description")}
|
||||||
|
data-testid="client-profile-description"
|
||||||
|
isReadOnly={!!globalProfile}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<ActionGroup>
|
||||||
|
{!globalProfile && (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={() => handleSubmit(save)()}
|
||||||
|
data-testid="saveCreateProfile"
|
||||||
|
isDisabled={!isDirty}
|
||||||
|
>
|
||||||
|
{t("common:save")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{editMode && !globalProfile && (
|
||||||
|
<Button
|
||||||
|
id={"reloadProfile"}
|
||||||
|
variant="link"
|
||||||
|
data-testid={"reloadProfile"}
|
||||||
|
isDisabled={!isDirty}
|
||||||
|
onClick={reload}
|
||||||
|
>
|
||||||
|
{t("realm-settings:reload")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{!editMode && !globalProfile && (
|
||||||
|
<Button
|
||||||
|
id={"cancelCreateProfile"}
|
||||||
|
component={(props) => (
|
||||||
|
<Link
|
||||||
|
{...props}
|
||||||
|
to={`/${realm}/realm-settings/clientPolicies`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
data-testid={"cancelCreateProfile"}
|
||||||
|
>
|
||||||
|
{t("common:cancel")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ActionGroup>
|
||||||
|
{editMode && (
|
||||||
|
<>
|
||||||
|
<Flex>
|
||||||
|
<FlexItem>
|
||||||
|
<Text className="kc-executors" component={TextVariants.h1}>
|
||||||
|
{t("executors")}
|
||||||
|
<HelpItem
|
||||||
|
helpText={t("realm-settings:executorsHelpText")}
|
||||||
|
forLabel={t("executorsHelpItem")}
|
||||||
|
forID={t("executors")}
|
||||||
|
/>
|
||||||
|
</Text>
|
||||||
|
</FlexItem>
|
||||||
|
{profile && (
|
||||||
|
<FlexItem align={{ default: "alignRight" }}>
|
||||||
|
<Button
|
||||||
|
id="addExecutor"
|
||||||
|
component={(props) => (
|
||||||
|
<Link
|
||||||
|
{...props}
|
||||||
|
to={toAddExecutor({
|
||||||
|
realm,
|
||||||
|
profileName,
|
||||||
|
})}
|
||||||
|
></Link>
|
||||||
|
)}
|
||||||
|
variant="link"
|
||||||
|
className="kc-addExecutor"
|
||||||
|
data-testid="cancelCreateProfile"
|
||||||
|
icon={<PlusCircleIcon />}
|
||||||
|
>
|
||||||
|
{t("realm-settings:addExecutor")}
|
||||||
|
</Button>
|
||||||
|
</FlexItem>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
{profileExecutors.length > 0 && (
|
||||||
|
<DataList aria-label={t("executors")} isCompact>
|
||||||
|
{profileExecutors.map((executor, idx) => (
|
||||||
|
<DataListItem
|
||||||
|
aria-labelledby={"executors-list-item"}
|
||||||
|
key={executor.executor}
|
||||||
|
id={executor.executor}
|
||||||
|
>
|
||||||
|
<DataListItemRow data-testid="executors-list-row">
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell
|
||||||
|
key="executor"
|
||||||
|
data-testid="executor-type"
|
||||||
|
>
|
||||||
|
{Object.keys(executor).length !== 0 ? (
|
||||||
|
<Link
|
||||||
|
data-testid="executor-type-link"
|
||||||
|
to={""}
|
||||||
|
className="kc-executor-link"
|
||||||
|
>
|
||||||
|
{executor.executor}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
executor.executor
|
||||||
|
)}
|
||||||
|
{executorTypes
|
||||||
|
?.filter(
|
||||||
|
(type) => type.id === executor.executor
|
||||||
|
)
|
||||||
|
.map((type) => (
|
||||||
|
<>
|
||||||
|
<HelpItem
|
||||||
|
key={type.id}
|
||||||
|
helpText={type.helpText}
|
||||||
|
forLabel={t("executorTypeTextHelpText")}
|
||||||
|
forID={t(`common:helpLabel`, {
|
||||||
|
label: t("executorTypeTextHelpText"),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
isInline
|
||||||
|
icon={
|
||||||
|
<TrashIcon
|
||||||
|
key={`executorType-trash-icon-${type.id}`}
|
||||||
|
className="kc-executor-trash-icon"
|
||||||
|
data-testid="deleteExecutor"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
toggleDeleteDialog();
|
||||||
|
setExecutorToDelete({
|
||||||
|
idx: idx,
|
||||||
|
name: type.id,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
></Button>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</DataListCell>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
))}
|
||||||
|
</DataList>
|
||||||
|
)}
|
||||||
|
{globalProfileExecutors.length > 0 && (
|
||||||
|
<>
|
||||||
|
<DataList aria-label={t("executors")} isCompact>
|
||||||
|
{globalProfileExecutors.map((executor) => (
|
||||||
|
<DataListItem
|
||||||
|
aria-labelledby={"global-executors-list-item"}
|
||||||
|
key={executor.executor}
|
||||||
|
id={executor.executor}
|
||||||
|
>
|
||||||
|
<DataListItemRow data-testid="global-executors-list-row">
|
||||||
|
<DataListItemCells
|
||||||
|
dataListCells={[
|
||||||
|
<DataListCell
|
||||||
|
key="executor"
|
||||||
|
data-testid="global-executor-type"
|
||||||
|
>
|
||||||
|
{Object.keys(executor).length !== 0 ? (
|
||||||
|
<Link
|
||||||
|
data-testid="global-executor-type-link"
|
||||||
|
to={""}
|
||||||
|
className="kc-global-executor-link"
|
||||||
|
>
|
||||||
|
{executor.executor}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
executor.executor
|
||||||
|
)}
|
||||||
|
{executorTypes
|
||||||
|
?.filter(
|
||||||
|
(type) => type.id === executor.executor
|
||||||
|
)
|
||||||
|
.map((type) => (
|
||||||
|
<HelpItem
|
||||||
|
key={type.id}
|
||||||
|
helpText={type.helpText}
|
||||||
|
forLabel={t("executorTypeTextHelpText")}
|
||||||
|
forID={t(`common:helpLabel`, {
|
||||||
|
label: t("executorTypeTextHelpText"),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</DataListCell>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</DataListItemRow>
|
||||||
|
</DataListItem>
|
||||||
|
))}
|
||||||
|
</DataList>
|
||||||
|
<Button
|
||||||
|
id="backToClientPolicies"
|
||||||
|
component={(props) => (
|
||||||
|
<Link
|
||||||
|
{...props}
|
||||||
|
to={`/${realm}/realm-settings/clientPolicies`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
variant="primary"
|
||||||
|
className="kc-backToPolicies"
|
||||||
|
data-testid="backToClientPolicies"
|
||||||
|
>
|
||||||
|
{t("realm-settings:back")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{profileExecutors.length === 0 &&
|
||||||
|
globalProfileExecutors.length === 0 && (
|
||||||
|
<>
|
||||||
|
<Divider />
|
||||||
|
<Text
|
||||||
|
className="kc-emptyExecutors"
|
||||||
|
component={TextVariants.h6}
|
||||||
|
>
|
||||||
|
{t("realm-settings:emptyExecutors")}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FormAccess>
|
||||||
|
</PageSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
207
src/realm-settings/ExecutorForm.tsx
Normal file
207
src/realm-settings/ExecutorForm.tsx
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
ActionGroup,
|
||||||
|
AlertVariant,
|
||||||
|
Button,
|
||||||
|
FormGroup,
|
||||||
|
PageSection,
|
||||||
|
Select,
|
||||||
|
SelectOption,
|
||||||
|
SelectVariant,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { FormAccess } from "../components/form-access/FormAccess";
|
||||||
|
import { ViewHeader } from "../components/view-header/ViewHeader";
|
||||||
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
|
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
|
||||||
|
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||||
|
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||||
|
import { Link, useHistory, useParams } from "react-router-dom";
|
||||||
|
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
|
||||||
|
import type ComponentTypeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/componentTypeRepresentation";
|
||||||
|
import type { ConfigPropertyRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigInfoRepresentation";
|
||||||
|
import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
|
||||||
|
import type { ClientProfileParams } from "./routes/ClientProfile";
|
||||||
|
import {
|
||||||
|
COMPONENTS,
|
||||||
|
isValidComponentType,
|
||||||
|
} from "../client-scopes/add/components/components";
|
||||||
|
import type ClientPolicyExecutorRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientPolicyExecutorRepresentation";
|
||||||
|
|
||||||
|
type ExecutorForm = Required<ClientPolicyExecutorRepresentation>;
|
||||||
|
|
||||||
|
const defaultValues: ExecutorForm = {
|
||||||
|
configuration: {},
|
||||||
|
executor: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExecutorForm = () => {
|
||||||
|
const { t } = useTranslation("realm-settings");
|
||||||
|
const history = useHistory();
|
||||||
|
const { realm, profileName } = useParams<ClientProfileParams>();
|
||||||
|
const { addAlert, addError } = useAlerts();
|
||||||
|
const [selectExecutorTypeOpen, setSelectExecutorTypeOpen] = useState(false);
|
||||||
|
const serverInfo = useServerInfo();
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
const executorTypes =
|
||||||
|
serverInfo.componentTypes?.[
|
||||||
|
"org.keycloak.services.clientpolicy.executor.ClientPolicyExecutorProvider"
|
||||||
|
];
|
||||||
|
const [executors, setExecutors] = useState<ComponentTypeRepresentation[]>([]);
|
||||||
|
const [executorProperties, setExecutorProperties] = useState<
|
||||||
|
ConfigPropertyRepresentation[]
|
||||||
|
>([]);
|
||||||
|
const [globalProfiles, setGlobalProfiles] = useState<
|
||||||
|
ClientProfileRepresentation[]
|
||||||
|
>([]);
|
||||||
|
const [profiles, setProfiles] = useState<ClientProfileRepresentation[]>([]);
|
||||||
|
const form = useForm<ExecutorForm>({ defaultValues });
|
||||||
|
const { control } = form;
|
||||||
|
|
||||||
|
useFetch(
|
||||||
|
() =>
|
||||||
|
adminClient.clientPolicies.listProfiles({ includeGlobalProfiles: true }),
|
||||||
|
(profiles) => {
|
||||||
|
setGlobalProfiles(profiles.globalProfiles ?? []);
|
||||||
|
setProfiles(profiles.profiles ?? []);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fldNameFormatter = (name: string) =>
|
||||||
|
name.toLowerCase().trim().split(/\s+/).join("-");
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
const formValues = form.getValues();
|
||||||
|
const updatedProfiles = profiles.map((profile) => {
|
||||||
|
if (profile.name !== profileName) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
const executors = (profile.executors ?? []).concat({
|
||||||
|
executor: formValues.executor,
|
||||||
|
configuration: formValues.configuration,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...profile,
|
||||||
|
executors,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await adminClient.clientPolicies.createProfiles({
|
||||||
|
profiles: updatedProfiles,
|
||||||
|
globalProfiles: globalProfiles,
|
||||||
|
});
|
||||||
|
addAlert(t("realm-settings:addExecutorSuccess"), AlertVariant.success);
|
||||||
|
history.push(`/${realm}/realm-settings/clientPolicies/${profileName}`);
|
||||||
|
} catch (error) {
|
||||||
|
addError("realm-settings:addExecutorError", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ViewHeader titleKey={t("addExecutor")} divider />
|
||||||
|
<PageSection variant="light">
|
||||||
|
<FormAccess isHorizontal role="manage-realm" className="pf-u-mt-lg">
|
||||||
|
<FormGroup
|
||||||
|
label={t("executorType")}
|
||||||
|
fieldId="kc-executorType"
|
||||||
|
labelIcon={
|
||||||
|
executors.length > 0 && executors[0].helpText! !== "" ? (
|
||||||
|
<HelpItem
|
||||||
|
helpText={executors[0].helpText}
|
||||||
|
forLabel={t("executorTypeHelpText")}
|
||||||
|
forID={t(`common:helpLabel`, {
|
||||||
|
label: t("executorTypeHelpText"),
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="executor"
|
||||||
|
defaultValue=""
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
toggleId="kc-executor"
|
||||||
|
placeholderText="Select an executor"
|
||||||
|
onToggle={(isOpen) => setSelectExecutorTypeOpen(isOpen)}
|
||||||
|
onSelect={(_, value) => {
|
||||||
|
onChange(value.toString());
|
||||||
|
const selectedExecutor = executorTypes?.filter(
|
||||||
|
(type) => type.id === value
|
||||||
|
);
|
||||||
|
setExecutors(selectedExecutor ?? []);
|
||||||
|
setExecutorProperties(
|
||||||
|
selectedExecutor?.[0].properties ?? []
|
||||||
|
);
|
||||||
|
setSelectExecutorTypeOpen(false);
|
||||||
|
}}
|
||||||
|
selections={value}
|
||||||
|
variant={SelectVariant.single}
|
||||||
|
data-testid="executorType-select"
|
||||||
|
aria-label={t("executorType")}
|
||||||
|
isOpen={selectExecutorTypeOpen}
|
||||||
|
maxHeight={580}
|
||||||
|
>
|
||||||
|
{executorTypes?.map((option) => (
|
||||||
|
<SelectOption
|
||||||
|
selected={option.id === value}
|
||||||
|
key={option.id}
|
||||||
|
value={option.id}
|
||||||
|
description={option.helpText}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormProvider {...form}>
|
||||||
|
{executorProperties.map((option) => {
|
||||||
|
const componentType = option.type!;
|
||||||
|
if (isValidComponentType(componentType)) {
|
||||||
|
const Component = COMPONENTS[componentType];
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
key={option.name}
|
||||||
|
{...option}
|
||||||
|
name={fldNameFormatter(option.label!)}
|
||||||
|
label={option.label}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`There is no editor registered for ${componentType}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
</FormProvider>
|
||||||
|
<ActionGroup>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={save}
|
||||||
|
data-testid="realm-settings-add-executor-save-button"
|
||||||
|
>
|
||||||
|
{t("common:add")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
component={(props) => (
|
||||||
|
<Link
|
||||||
|
{...props}
|
||||||
|
to={`/${realm}/realm-settings/clientPolicies/${profileName}`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
data-testid="realm-settings-add-executor-cancel-button"
|
||||||
|
>
|
||||||
|
{t("common:cancel")}
|
||||||
|
</Button>
|
||||||
|
</ActionGroup>
|
||||||
|
</FormAccess>
|
||||||
|
</PageSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,234 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import {
|
|
||||||
ActionGroup,
|
|
||||||
AlertVariant,
|
|
||||||
Button,
|
|
||||||
ButtonVariant,
|
|
||||||
Divider,
|
|
||||||
DropdownItem,
|
|
||||||
Flex,
|
|
||||||
FlexItem,
|
|
||||||
FormGroup,
|
|
||||||
PageSection,
|
|
||||||
Text,
|
|
||||||
TextArea,
|
|
||||||
TextInput,
|
|
||||||
TextVariants,
|
|
||||||
ValidatedOptions,
|
|
||||||
} from "@patternfly/react-core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { FormAccess } from "../components/form-access/FormAccess";
|
|
||||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
|
||||||
import { Link, useHistory } from "react-router-dom";
|
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
|
||||||
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
|
|
||||||
import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
|
|
||||||
import { HelpItem } from "../components/help-enabler/HelpItem";
|
|
||||||
import { PlusCircleIcon } from "@patternfly/react-icons";
|
|
||||||
import "./RealmSettingsSection.css";
|
|
||||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
|
||||||
|
|
||||||
type NewClientProfileForm = Required<ClientProfileRepresentation>;
|
|
||||||
|
|
||||||
const defaultValues: NewClientProfileForm = {
|
|
||||||
name: "",
|
|
||||||
description: "",
|
|
||||||
executors: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NewClientProfileForm = () => {
|
|
||||||
const { t } = useTranslation("realm-settings");
|
|
||||||
const { getValues, register, errors } = useForm<NewClientProfileForm>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
const { realm } = useRealm();
|
|
||||||
const { addAlert, addError } = useAlerts();
|
|
||||||
const adminClient = useAdminClient();
|
|
||||||
const [globalProfiles, setGlobalProfiles] = useState<
|
|
||||||
ClientProfileRepresentation[]
|
|
||||||
>([]);
|
|
||||||
const [profiles, setProfiles] = useState<ClientProfileRepresentation[]>([]);
|
|
||||||
const [showAddExecutorsForm, setShowAddExecutorsForm] = useState(false);
|
|
||||||
const [createdProfile, setCreatedProfile] =
|
|
||||||
useState<ClientProfileRepresentation>();
|
|
||||||
const form = getValues();
|
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
useFetch(
|
|
||||||
() =>
|
|
||||||
adminClient.clientPolicies.listProfiles({ includeGlobalProfiles: true }),
|
|
||||||
(profiles) => {
|
|
||||||
setGlobalProfiles(profiles.globalProfiles ?? []);
|
|
||||||
setProfiles(profiles.profiles ?? []);
|
|
||||||
},
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const save = async () => {
|
|
||||||
const form = getValues();
|
|
||||||
|
|
||||||
const createdProfile = {
|
|
||||||
...form,
|
|
||||||
executors: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const allProfiles = profiles.concat(createdProfile);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await adminClient.clientPolicies.createProfiles({
|
|
||||||
profiles: allProfiles,
|
|
||||||
globalProfiles: globalProfiles,
|
|
||||||
});
|
|
||||||
addAlert(
|
|
||||||
t("realm-settings:createClientProfileSuccess"),
|
|
||||||
AlertVariant.success
|
|
||||||
);
|
|
||||||
setShowAddExecutorsForm(true);
|
|
||||||
setCreatedProfile(createdProfile);
|
|
||||||
} catch (error) {
|
|
||||||
addError("realm-settings:createClientProfileError", error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
|
||||||
titleKey: t("deleteClientProfileConfirmTitle"),
|
|
||||||
messageKey: t("deleteClientProfileConfirm"),
|
|
||||||
continueButtonLabel: t("delete"),
|
|
||||||
continueButtonVariant: ButtonVariant.danger,
|
|
||||||
onConfirm: async () => {
|
|
||||||
const updatedProfiles = profiles.filter(
|
|
||||||
(profile) => profile.name !== createdProfile?.name
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await adminClient.clientPolicies.createProfiles({
|
|
||||||
profiles: updatedProfiles,
|
|
||||||
globalProfiles,
|
|
||||||
});
|
|
||||||
addAlert(t("deleteClientSuccess"), AlertVariant.success);
|
|
||||||
history.push(`/${realm}/realm-settings/clientPolicies`);
|
|
||||||
} catch (error) {
|
|
||||||
addError(t("deleteClientError"), error);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DeleteConfirm />
|
|
||||||
<ViewHeader
|
|
||||||
titleKey={showAddExecutorsForm ? form.name : t("newClientProfile")}
|
|
||||||
divider
|
|
||||||
dropdownItems={
|
|
||||||
showAddExecutorsForm
|
|
||||||
? [
|
|
||||||
<DropdownItem
|
|
||||||
key="delete"
|
|
||||||
value="delete"
|
|
||||||
onClick={toggleDeleteDialog}
|
|
||||||
data-testid="deleteClientProfileDropdown"
|
|
||||||
>
|
|
||||||
{t("deleteClientProfile")}
|
|
||||||
</DropdownItem>,
|
|
||||||
]
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<PageSection variant="light">
|
|
||||||
<FormAccess isHorizontal role="view-realm" className="pf-u-mt-lg">
|
|
||||||
<FormGroup
|
|
||||||
label={t("newClientProfileName")}
|
|
||||||
fieldId="kc-name"
|
|
||||||
helperText={t("createClientProfileNameHelperText")}
|
|
||||||
isRequired
|
|
||||||
helperTextInvalid={t("common:required")}
|
|
||||||
validated={
|
|
||||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TextInput
|
|
||||||
ref={register({ required: true })}
|
|
||||||
type="text"
|
|
||||||
id="kc-client-profile-name"
|
|
||||||
name="name"
|
|
||||||
data-testid="client-profile-name"
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<FormGroup label={t("common:description")} fieldId="kc-description">
|
|
||||||
<TextArea
|
|
||||||
name="description"
|
|
||||||
aria-label={t("description")}
|
|
||||||
ref={register()}
|
|
||||||
type="text"
|
|
||||||
id="kc-client-profile-description"
|
|
||||||
data-testid="client-profile-description"
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<ActionGroup>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={save}
|
|
||||||
data-testid="saveCreateProfile"
|
|
||||||
isDisabled={showAddExecutorsForm ? true : false}
|
|
||||||
>
|
|
||||||
{t("common:save")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
id="cancelCreateProfile"
|
|
||||||
component={(props) => (
|
|
||||||
<Link
|
|
||||||
{...props}
|
|
||||||
to={`/${realm}/realm-settings/clientPolicies`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
data-testid="cancelCreateProfile"
|
|
||||||
>
|
|
||||||
{showAddExecutorsForm
|
|
||||||
? t("realm-settings:reload")
|
|
||||||
: t("common:cancel")}
|
|
||||||
</Button>
|
|
||||||
</ActionGroup>
|
|
||||||
{showAddExecutorsForm && (
|
|
||||||
<>
|
|
||||||
<Flex>
|
|
||||||
<FlexItem>
|
|
||||||
<Text className="kc-executors" component={TextVariants.h1}>
|
|
||||||
{t("executors")}
|
|
||||||
<HelpItem
|
|
||||||
helpText={t("realm-settings:executorsHelpText")}
|
|
||||||
forLabel={t("executorsHelpItem")}
|
|
||||||
forID={t("executors")}
|
|
||||||
/>
|
|
||||||
</Text>
|
|
||||||
</FlexItem>
|
|
||||||
<FlexItem align={{ default: "alignRight" }}>
|
|
||||||
<Button
|
|
||||||
id="addExecutor"
|
|
||||||
component={(props) => (
|
|
||||||
<Link
|
|
||||||
{...props}
|
|
||||||
to={`/${realm}/realm-settings/clientPolicies`}
|
|
||||||
></Link>
|
|
||||||
)}
|
|
||||||
variant="link"
|
|
||||||
className="kc-addExecutor"
|
|
||||||
data-testid="cancelCreateProfile"
|
|
||||||
icon={<PlusCircleIcon />}
|
|
||||||
>
|
|
||||||
{t("realm-settings:addExecutor")}
|
|
||||||
</Button>
|
|
||||||
</FlexItem>
|
|
||||||
</Flex>
|
|
||||||
<Divider />
|
|
||||||
<Text className="kc-emptyExecutors" component={TextVariants.h6}>
|
|
||||||
{t("realm-settings:emptyExecutors")}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</FormAccess>
|
|
||||||
</PageSection>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -22,10 +22,10 @@ import { useRealm } from "../context/realm-context/RealmContext";
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
import { prettyPrintJSON } from "../util";
|
import { prettyPrintJSON } from "../util";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { toNewClientProfile } from "./routes/NewClientProfile";
|
import { toAddClientProfile } from "./routes/AddClientProfile";
|
||||||
import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
|
import type ClientProfileRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientProfileRepresentation";
|
||||||
|
|
||||||
import "./RealmSettingsSection.css";
|
import "./RealmSettingsSection.css";
|
||||||
|
import { toClientProfile } from "./routes/ClientProfile";
|
||||||
|
|
||||||
type ClientProfile = ClientProfileRepresentation & {
|
type ClientProfile = ClientProfileRepresentation & {
|
||||||
global: boolean;
|
global: boolean;
|
||||||
|
@ -79,7 +79,9 @@ export const ProfilesTab = () => {
|
||||||
|
|
||||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
titleKey: t("deleteClientProfileConfirmTitle"),
|
titleKey: t("deleteClientProfileConfirmTitle"),
|
||||||
messageKey: t("deleteClientProfileConfirm"),
|
messageKey: t("deleteClientProfileConfirm", {
|
||||||
|
profileName: selectedProfile?.name,
|
||||||
|
}),
|
||||||
continueButtonLabel: t("delete"),
|
continueButtonLabel: t("delete"),
|
||||||
continueButtonVariant: ButtonVariant.danger,
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
|
@ -105,7 +107,13 @@ export const ProfilesTab = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const cellFormatter = (row: ClientProfile) => (
|
const cellFormatter = (row: ClientProfile) => (
|
||||||
<Link to={""} key={row.name}>
|
<Link
|
||||||
|
to={toClientProfile({
|
||||||
|
realm,
|
||||||
|
profileName: row.name!,
|
||||||
|
})}
|
||||||
|
key={row.name}
|
||||||
|
>
|
||||||
{row.name} {row.global && <Label color="blue">{t("global")}</Label>}
|
{row.name} {row.global && <Label color="blue">{t("global")}</Label>}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
@ -197,7 +205,7 @@ export const ProfilesTab = () => {
|
||||||
<Button
|
<Button
|
||||||
id="createProfile"
|
id="createProfile"
|
||||||
component={(props) => (
|
component={(props) => (
|
||||||
<Link {...props} to={toNewClientProfile({ realm })} />
|
<Link {...props} to={toAddClientProfile({ realm })} />
|
||||||
)}
|
)}
|
||||||
data-testid="createProfile"
|
data-testid="createProfile"
|
||||||
>
|
>
|
||||||
|
|
|
@ -210,6 +210,23 @@ article.pf-c-card.pf-m-flat.kc-login-settings-template
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.kc-executor-link {
|
||||||
|
margin-right: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-backToPolicies {
|
||||||
|
width: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-executor-trash-icon {
|
||||||
|
margin-left: .5rem;
|
||||||
|
color: var(--pf-global--Color--200);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-executor-trash-icon:hover {
|
||||||
|
filter: brightness(55%);
|
||||||
|
}
|
||||||
|
|
||||||
.kc-condition-link {
|
.kc-condition-link {
|
||||||
margin-right: 0.625rem;
|
margin-right: 0.625rem;
|
||||||
}
|
}
|
||||||
|
|
|
@ -238,18 +238,22 @@ export default {
|
||||||
"There are no profiles, select 'Create client profile' to create a new client profile",
|
"There are no profiles, select 'Create client profile' to create a new client profile",
|
||||||
deleteClientProfileConfirmTitle: "Delete profile?",
|
deleteClientProfileConfirmTitle: "Delete profile?",
|
||||||
deleteClientProfileConfirm:
|
deleteClientProfileConfirm:
|
||||||
"This action will permanently delete the profile custom-profile. This cannot be undone.",
|
"This action will permanently delete the profile {{profileName}}. This cannot be undone.",
|
||||||
deleteClientSuccess: "Client profile deleted",
|
deleteClientSuccess: "Client profile deleted",
|
||||||
deleteClientError: "Could not delete profile: {{error}}",
|
deleteClientError: "Could not delete profile: {{error}}",
|
||||||
createClientProfile: "Create client profile",
|
createClientProfile: "Create client profile",
|
||||||
deleteClientProfile: "Delete this client profile",
|
deleteClientProfile: "Delete this client profile",
|
||||||
createClientProfileSuccess: "New client profile created",
|
createClientProfileSuccess: "New client profile created",
|
||||||
|
updateClientProfileSuccess: "Client profile updated successfully",
|
||||||
createClientProfileError: "Could not create client profile: '{{error}}'",
|
createClientProfileError: "Could not create client profile: '{{error}}'",
|
||||||
|
updateClientProfileError: "Could not update client profile: '{{error}}'",
|
||||||
createClientProfileNameHelperText:
|
createClientProfileNameHelperText:
|
||||||
"The name must be unique within the realm",
|
"The name must be unique within the realm",
|
||||||
allClientPolicies: "Client policies",
|
allClientPolicies: "Client policies",
|
||||||
newClientProfile: "Create client profile",
|
newClientProfile: "Create client profile",
|
||||||
newClientProfileName: "Client profile name",
|
newClientProfileName: "Client profile name",
|
||||||
|
clientProfile: "Client profile details",
|
||||||
|
back: "Back",
|
||||||
delete: "delete",
|
delete: "delete",
|
||||||
save: "Save",
|
save: "Save",
|
||||||
reload: "Reload",
|
reload: "Reload",
|
||||||
|
@ -260,7 +264,24 @@ export default {
|
||||||
"Executors, which will be applied for this client profile",
|
"Executors, which will be applied for this client profile",
|
||||||
executorsHelpItem: "Executors help item",
|
executorsHelpItem: "Executors help item",
|
||||||
addExecutor: "Add executor",
|
addExecutor: "Add executor",
|
||||||
|
executorType: "Executor type",
|
||||||
|
executorTypeSwitchHelpText: "Executor Type Switch Help Text",
|
||||||
|
executorTypeSelectHelpText: "Executor Type Select Help Text",
|
||||||
|
executorTypeSelectAlgorithm: "Executor Type Select Algorithm",
|
||||||
|
executorTypeTextHelpText: "Executor Type Text Help Text",
|
||||||
|
executorAuthenticatorMultiSelectHelpText:
|
||||||
|
"Executor Authenticator MultiSelect Help Text",
|
||||||
|
executorClientAuthenticator: "Executor Client Authenticator",
|
||||||
|
executorsTable: "Executors table",
|
||||||
|
executorName: "Name",
|
||||||
emptyExecutors: "No executors configured",
|
emptyExecutors: "No executors configured",
|
||||||
|
addExecutorSuccess: "Success! Executor created successfully",
|
||||||
|
addExecutorError: "Executor not created",
|
||||||
|
deleteExecutorProfileConfirmTitle: "Delete executor?",
|
||||||
|
deleteExecutorProfileConfirm:
|
||||||
|
"The action will permanently delete {{executorName}}. This cannot be undone.",
|
||||||
|
deleteExecutorSuccess: "Success! The executor was deleted.",
|
||||||
|
deleteExecutorError: "Could not delete executor: {{error}}",
|
||||||
updateClientProfilesSuccess:
|
updateClientProfilesSuccess:
|
||||||
"The client profiles configuration was updated",
|
"The client profiles configuration was updated",
|
||||||
updateClientProfilesError:
|
updateClientProfilesError:
|
||||||
|
|
|
@ -6,7 +6,10 @@ import { JavaKeystoreSettingsRoute } from "./routes/JavaKeystoreSettings";
|
||||||
import { RealmSettingsRoute } from "./routes/RealmSettings";
|
import { RealmSettingsRoute } from "./routes/RealmSettings";
|
||||||
import { RsaGeneratedSettingsRoute } from "./routes/RsaGeneratedSettings";
|
import { RsaGeneratedSettingsRoute } from "./routes/RsaGeneratedSettings";
|
||||||
import { RsaSettingsRoute } from "./routes/RsaSettings";
|
import { RsaSettingsRoute } from "./routes/RsaSettings";
|
||||||
import { NewClientProfileRoute } from "./routes/NewClientProfile";
|
import { ClientPoliciesRoute } from "./routes/ClientPolicies";
|
||||||
|
import { AddClientProfileRoute } from "./routes/AddClientProfile";
|
||||||
|
import { ClientProfileRoute } from "./routes/ClientProfile";
|
||||||
|
import { AddExecutorRoute } from "./routes/AddExecutor";
|
||||||
import { NewClientPolicyRoute } from "./routes/NewClientPolicy";
|
import { NewClientPolicyRoute } from "./routes/NewClientPolicy";
|
||||||
import { EditClientPolicyRoute } from "./routes/EditClientPolicy";
|
import { EditClientPolicyRoute } from "./routes/EditClientPolicy";
|
||||||
import { NewClientPolicyConditionRoute } from "./routes/AddCondition";
|
import { NewClientPolicyConditionRoute } from "./routes/AddCondition";
|
||||||
|
@ -19,7 +22,10 @@ const routes: RouteDef[] = [
|
||||||
JavaKeystoreSettingsRoute,
|
JavaKeystoreSettingsRoute,
|
||||||
RsaGeneratedSettingsRoute,
|
RsaGeneratedSettingsRoute,
|
||||||
RsaSettingsRoute,
|
RsaSettingsRoute,
|
||||||
NewClientProfileRoute,
|
ClientPoliciesRoute,
|
||||||
|
AddClientProfileRoute,
|
||||||
|
AddExecutorRoute,
|
||||||
|
ClientProfileRoute,
|
||||||
NewClientPolicyRoute,
|
NewClientPolicyRoute,
|
||||||
EditClientPolicyRoute,
|
EditClientPolicyRoute,
|
||||||
NewClientPolicyConditionRoute,
|
NewClientPolicyConditionRoute,
|
||||||
|
|
21
src/realm-settings/routes/AddClientProfile.ts
Normal file
21
src/realm-settings/routes/AddClientProfile.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import type { LocationDescriptorObject } from "history";
|
||||||
|
import { generatePath } from "react-router-dom";
|
||||||
|
import type { RouteDef } from "../../route-config";
|
||||||
|
import { ClientProfileForm } from "../ClientProfileForm";
|
||||||
|
|
||||||
|
export type AddClientProfileParams = {
|
||||||
|
realm: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddClientProfileRoute: RouteDef = {
|
||||||
|
path: "/:realm/realm-settings/clientPolicies/add-profile",
|
||||||
|
component: ClientProfileForm,
|
||||||
|
breadcrumb: (t) => t("realm-settings:newClientProfile"),
|
||||||
|
access: "manage-realm",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toAddClientProfile = (
|
||||||
|
params: AddClientProfileParams
|
||||||
|
): LocationDescriptorObject => ({
|
||||||
|
pathname: generatePath(AddClientProfileRoute.path, params),
|
||||||
|
});
|
22
src/realm-settings/routes/AddExecutor.ts
Normal file
22
src/realm-settings/routes/AddExecutor.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import type { LocationDescriptorObject } from "history";
|
||||||
|
import { generatePath } from "react-router-dom";
|
||||||
|
import type { RouteDef } from "../../route-config";
|
||||||
|
import { ExecutorForm } from "../ExecutorForm";
|
||||||
|
|
||||||
|
export type AddExecutorParams = {
|
||||||
|
realm: string;
|
||||||
|
profileName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AddExecutorRoute: RouteDef = {
|
||||||
|
path: "/:realm/realm-settings/clientPolicies/:profileName/add-executor",
|
||||||
|
component: ExecutorForm,
|
||||||
|
breadcrumb: (t) => t("realm-settings:addExecutor"),
|
||||||
|
access: "manage-realm",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toAddExecutor = (
|
||||||
|
params: AddExecutorParams
|
||||||
|
): LocationDescriptorObject => ({
|
||||||
|
pathname: generatePath(AddExecutorRoute.path, params),
|
||||||
|
});
|
22
src/realm-settings/routes/ClientProfile.ts
Normal file
22
src/realm-settings/routes/ClientProfile.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import type { LocationDescriptorObject } from "history";
|
||||||
|
import { generatePath } from "react-router-dom";
|
||||||
|
import type { RouteDef } from "../../route-config";
|
||||||
|
import { ClientProfileForm } from "../ClientProfileForm";
|
||||||
|
|
||||||
|
export type ClientProfileParams = {
|
||||||
|
realm: string;
|
||||||
|
profileName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClientProfileRoute: RouteDef = {
|
||||||
|
path: "/:realm/realm-settings/clientPolicies/:profileName",
|
||||||
|
component: ClientProfileForm,
|
||||||
|
breadcrumb: (t) => t("realm-settings:clientProfile"),
|
||||||
|
access: ["view-realm", "view-users"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toClientProfile = (
|
||||||
|
params: ClientProfileParams
|
||||||
|
): LocationDescriptorObject => ({
|
||||||
|
pathname: generatePath(ClientProfileRoute.path, params),
|
||||||
|
});
|
|
@ -1,21 +0,0 @@
|
||||||
import type { LocationDescriptorObject } from "history";
|
|
||||||
import { generatePath } from "react-router-dom";
|
|
||||||
import type { RouteDef } from "../../route-config";
|
|
||||||
import { NewClientProfileForm } from "../NewClientProfileForm";
|
|
||||||
|
|
||||||
export type NewClientProfileParams = {
|
|
||||||
realm: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NewClientProfileRoute: RouteDef = {
|
|
||||||
path: "/:realm/realm-settings/clientPolicies/new-client-profile",
|
|
||||||
component: NewClientProfileForm,
|
|
||||||
breadcrumb: (t) => t("realm-settings:newClientProfile"),
|
|
||||||
access: "view-realm",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toNewClientProfile = (
|
|
||||||
params: NewClientProfileParams
|
|
||||||
): LocationDescriptorObject => ({
|
|
||||||
pathname: generatePath(NewClientProfileRoute.path, params),
|
|
||||||
});
|
|
Loading…
Reference in a new issue