Skip to content

Commit

Permalink
Allow navigation to policy on permission screen (keycloak#21411)
Browse files Browse the repository at this point in the history
  • Loading branch information
andreas-blaettlinger committed Jul 17, 2023
1 parent 7c50b10 commit cd732ae
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default class PermissionsTab extends CommonPage {
.parent()
.parent()
.findByText(name)
.parent()
.click();
return this;
}
Expand Down
4 changes: 3 additions & 1 deletion js/apps/admin-ui/public/locales/en/clients.json
Original file line number Diff line number Diff line change
Expand Up @@ -543,5 +543,7 @@
"never": "Never expires"
},
"mappers": "Mappers",
"sessions": "Sessions"
"sessions": "Sessions",
"unsavedChangesTitle": "Unsaved changes",
"unsavedChangesConfirm": "You have unsaved changes. Do you really want to leave the page?"
}
181 changes: 134 additions & 47 deletions js/apps/admin-ui/src/clients/authorization/ResourcesPolicySelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,28 @@ import type {
Clients,
PolicyQuery,
} from "@keycloak/keycloak-admin-client/lib/resources/clients";
import { Select, SelectOption, SelectVariant } from "@patternfly/react-core";
import {
ButtonVariant,
Chip,
ChipGroup,
Select,
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import {
Controller,
ControllerRenderProps,
useFormContext,
} from "react-hook-form";
import { useTranslation } from "react-i18next";

import { Link, useNavigate } from "react-router-dom";
import { adminClient } from "../../admin-client";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useFetch } from "../../utils/useFetch";
import { toPolicyDetails } from "../routes/PolicyDetails";
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";

type Type = "resources" | "policies";

Expand All @@ -26,6 +41,7 @@ type ResourcesPolicySelectProps = {
type Policies = {
id?: string;
name?: string;
type?: string;
};

type TypeMapping = {
Expand Down Expand Up @@ -57,15 +73,18 @@ export const ResourcesPolicySelect = ({
preSelected,
isRequired = false,
}: ResourcesPolicySelectProps) => {
const { realm } = useRealm();
const { t } = useTranslation("clients");
const navigate = useNavigate();

const {
control,
formState: { errors },
formState: { errors, isDirty },
} = useFormContext<PolicyRepresentation>();
const [items, setItems] = useState<Policies[]>([]);
const [search, setSearch] = useState("");
const [open, setOpen] = useState(false);
const [clickedPolicy, setClickedPolicy] = useState<Policies>();

const functions = typeMapping[name];

Expand All @@ -74,6 +93,7 @@ export const ResourcesPolicySelect = ({
): Policies => ({
id: "_id" in p ? p._id : "id" in p ? p.id : undefined,
name: p.name,
type: p.type,
});

useFetch(
Expand Down Expand Up @@ -108,57 +128,124 @@ export const ResourcesPolicySelect = ({
[search],
);

const [toggleUnsavedChangesDialog, UnsavedChangesConfirm] = useConfirmDialog({
titleKey: t("unsavedChangesTitle"),
messageKey: t("unsavedChangesConfirm"),
continueButtonLabel: t("common:continue"),
continueButtonVariant: ButtonVariant.danger,
onConfirm: () => navigate(to(clickedPolicy!)),
});

const to = (policy: Policies) =>
toPolicyDetails({
realm: realm,
id: clientId,
policyId: policy.id!,
policyType: policy.type!,
});

const toSelectOptions = () =>
items.map((p) => (
<SelectOption key={p.id} value={p.id}>
{p.name}
</SelectOption>
));

const toChipGroupItems = (
field: ControllerRenderProps<PolicyRepresentation, Type>,
) => {
return (
<ChipGroup>
{field.value?.map((permissionId) => {
const policy = items.find(
(permission) => permission.id === permissionId,
);

if (!policy) {
return null;
}

return (
<Chip
key={policy.id}
onClick={(event) => {
event.stopPropagation();
field.onChange(field.value?.filter((id) => id !== policy.id));
}}
>
{policy.type ? (
<Link
to={to(policy)}
onClick={(event) => {
if (isDirty) {
event.preventDefault();
event.stopPropagation();
setOpen(false);
setClickedPolicy(policy);
toggleUnsavedChangesDialog();
}
}}
>
{policy.name}
</Link>
) : (
policy.name
)}
</Chip>
);
})}
</ChipGroup>
);
};

return (
<Controller
name={name}
defaultValue={preSelected ? [preSelected] : []}
control={control}
rules={{ validate: (value) => !isRequired || value!.length > 0 }}
render={({ field }) => (
<Select
toggleId={name}
variant={variant}
onToggle={setOpen}
onFilter={(_, filter) => {
setSearch(filter);
return toSelectOptions();
}}
onClear={() => {
field.onChange([]);
setSearch("");
}}
selections={field.value}
onSelect={(_, selectedValue) => {
const option = selectedValue.toString();
if (variant === SelectVariant.typeaheadMulti) {
const changedValue = field.value?.find(
(p: string) => p === option,
)
? field.value.filter((p: string) => p !== option)
: [...field.value!, option];
field.onChange(changedValue);
} else {
field.onChange([option]);
}

setSearch("");
}}
isOpen={open}
aria-labelledby={t(name)}
isDisabled={!!preSelected}
validated={errors[name] ? "error" : "default"}
typeAheadAriaLabel={t(name)}
>
{toSelectOptions()}
</Select>
)}
/>
<>
<UnsavedChangesConfirm />
<Controller
name={name}
defaultValue={preSelected ? [preSelected] : []}
control={control}
rules={{ validate: (value) => !isRequired || value!.length > 0 }}
render={({ field }) => (
<Select
toggleId={name}
variant={variant}
onToggle={setOpen}
onFilter={(_, filter) => {
setSearch(filter);
return toSelectOptions();
}}
onClear={() => {
field.onChange([]);
setSearch("");
}}
selections={field.value}
onSelect={(_, selectedValue) => {
const option = selectedValue.toString();
if (variant === SelectVariant.typeaheadMulti) {
const changedValue = field.value?.find(
(p: string) => p === option,
)
? field.value.filter((p: string) => p !== option)
: [...field.value!, option];
field.onChange(changedValue);
} else {
field.onChange([option]);
}

setSearch("");
}}
isOpen={open}
aria-labelledby={t(name)}
isDisabled={!!preSelected}
validated={errors[name] ? "error" : "default"}
typeAheadAriaLabel={t(name)}
chipGroupComponent={toChipGroupItems(field)}
>
{toSelectOptions()}
</Select>
)}
/>
</>
);
};

0 comments on commit cd732ae

Please sign in to comment.