Refactored a little and fixed bug (#2918)
This commit is contained in:
parent
ab23d21626
commit
6caa64466e
11 changed files with 705 additions and 694 deletions
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -7,7 +7,7 @@
|
|||
"name": "keycloak-admin-ui",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@keycloak/keycloak-admin-client": "^19.0.0-dev.15",
|
||||
"@keycloak/keycloak-admin-client": "^19.0.0-dev.16",
|
||||
"@patternfly/patternfly": "^4.202.1",
|
||||
"@patternfly/react-code-editor": "^4.65.1",
|
||||
"@patternfly/react-core": "^4.224.1",
|
||||
|
@ -2409,9 +2409,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@keycloak/keycloak-admin-client": {
|
||||
"version": "19.0.0-dev.15",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-19.0.0-dev.15.tgz",
|
||||
"integrity": "sha512-+NY+afR/Pov9rrOygooFkjrTDzP3auOzQunQQ1mfx2NgG1mk2+uSNehUj0ToWL2AcsdF8s/Z3QlP3Q+BCSUEFg==",
|
||||
"version": "19.0.0-dev.16",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-19.0.0-dev.16.tgz",
|
||||
"integrity": "sha512-WSsP+vIvhsf3uQhjYbpVMvuFcCSFwbqOzFyPXtsoCOrW+BPUwLxexkl5pOWGmeqCXDZjxxlCF+8StmCl5zPoMQ==",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
"camelize-ts": "^1.0.8",
|
||||
|
@ -20126,9 +20126,9 @@
|
|||
}
|
||||
},
|
||||
"@keycloak/keycloak-admin-client": {
|
||||
"version": "19.0.0-dev.15",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-19.0.0-dev.15.tgz",
|
||||
"integrity": "sha512-+NY+afR/Pov9rrOygooFkjrTDzP3auOzQunQQ1mfx2NgG1mk2+uSNehUj0ToWL2AcsdF8s/Z3QlP3Q+BCSUEFg==",
|
||||
"version": "19.0.0-dev.16",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-19.0.0-dev.16.tgz",
|
||||
"integrity": "sha512-WSsP+vIvhsf3uQhjYbpVMvuFcCSFwbqOzFyPXtsoCOrW+BPUwLxexkl5pOWGmeqCXDZjxxlCF+8StmCl5zPoMQ==",
|
||||
"requires": {
|
||||
"axios": "^0.27.2",
|
||||
"camelize-ts": "^1.0.8",
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"server:import-client": "./scripts/import-client.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@keycloak/keycloak-admin-client": "^19.0.0-dev.15",
|
||||
"@keycloak/keycloak-admin-client": "^19.0.0-dev.16",
|
||||
"@patternfly/patternfly": "^4.202.1",
|
||||
"@patternfly/react-code-editor": "^4.65.1",
|
||||
"@patternfly/react-core": "^4.224.1",
|
||||
|
|
|
@ -69,6 +69,7 @@
|
|||
"fineGrainSamlEndpointConfig": "This section to configure exact URLs for Assertion Consumer and Single Logout Service.",
|
||||
"logoUrl": "URL that references a logo for the Client application",
|
||||
"policyUrl": "URL that the Relying Party Client provides to the End-User to read about the how the profile data will be used",
|
||||
"policyUsers": "Specifies which user(s) are allowed by this policy.",
|
||||
"termsOfServiceUrl": "URL that the Relying Party Client provides to the End-User to read about the Relying Party's terms of service",
|
||||
"accessTokenSignatureAlgorithm": "JWA algorithm used for signing access tokens.",
|
||||
"idTokenSignatureAlgorithm": "JWA algorithm used for signing ID tokens.",
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"tooltip": "Name of stored user attribute which is the name of an attribute within the UserModel.attribute map."
|
||||
},
|
||||
"clientRoleMapping": {
|
||||
"client": {
|
||||
"clientId": {
|
||||
"label": "Client ID",
|
||||
"tooltip": "Client ID for role mappings. Just client roles of this client will be added to the token. If this is unset, client roles of all clients will be added to the token."
|
||||
},
|
||||
|
|
|
@ -9,7 +9,7 @@ import {
|
|||
TextVariants,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import type AccessTokenRepresentation from "@keycloak/keycloak-admin-client/lib/defs/accessTokenAuthorization";
|
||||
import type AccessTokenRepresentation from "@keycloak/keycloak-admin-client/lib/defs/accessTokenRepresentation";
|
||||
import { KeycloakTextArea } from "../../components/keycloak-text-area/KeycloakTextArea";
|
||||
import useToggle from "../../utils/useToggle";
|
||||
import { prettyPrintJSON } from "../../util";
|
||||
|
@ -36,6 +36,7 @@ export const AuthorizationDataModal = ({
|
|||
<Modal
|
||||
variant={ModalVariant.medium}
|
||||
isOpen={show}
|
||||
aria-label={t("authData")}
|
||||
header={
|
||||
<TextContent>
|
||||
<Text component={TextVariants.h1}>{t("authData")}</Text>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||
import React, { useState, KeyboardEvent, useMemo, useRef } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import {
|
||||
FormGroup,
|
||||
Select,
|
||||
|
@ -11,41 +11,32 @@ import {
|
|||
Button,
|
||||
Switch,
|
||||
ExpandableSection,
|
||||
TextInput,
|
||||
ButtonVariant,
|
||||
InputGroup,
|
||||
Toolbar,
|
||||
ToolbarGroup,
|
||||
ToolbarItem,
|
||||
Divider,
|
||||
} from "@patternfly/react-core";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
||||
import type EvaluationResultRepresentation from "@keycloak/keycloak-admin-client/lib/defs/evaluationResultRepresentation";
|
||||
import type ResourceEvaluation from "@keycloak/keycloak-admin-client/lib/defs/resourceEvaluation";
|
||||
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
||||
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
|
||||
import type PolicyEvaluationResponse from "@keycloak/keycloak-admin-client/lib/defs/policyEvaluationResponse";
|
||||
|
||||
import type { KeyValueType } from "../../components/key-value-form/key-value-convert";
|
||||
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { HelpItem } from "../../components/help-enabler/HelpItem";
|
||||
import { FormPanel } from "../../components/scroll-form/FormPanel";
|
||||
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
||||
import type AccessTokenRepresentation from "@keycloak/keycloak-admin-client/lib/defs/accessTokenAuthorization";
|
||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||
import type ResourceEvaluation from "@keycloak/keycloak-admin-client/lib/defs/resourceEvaluation";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { KeyBasedAttributeInput } from "./KeyBasedAttributeInput";
|
||||
import { defaultContextAttributes } from "../utils";
|
||||
import type EvaluationResultRepresentation from "@keycloak/keycloak-admin-client/lib/defs/evaluationResultRepresentation";
|
||||
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
||||
import type ScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/scopeRepresentation";
|
||||
import type { KeyValueType } from "../../components/key-value-form/key-value-convert";
|
||||
import { TableComposable, Th, Thead, Tr } from "@patternfly/react-table";
|
||||
import { AuthorizationEvaluateResource } from "./AuthorizationEvaluateResource";
|
||||
import { SearchIcon } from "@patternfly/react-icons";
|
||||
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
import { ForbiddenSection } from "../../ForbiddenSection";
|
||||
import { AuthorizationDataModal } from "./AuthorizationDataModal";
|
||||
import { Results } from "./evaluate/Results";
|
||||
import { ClientSelect } from "../../components/client/ClientSelect";
|
||||
|
||||
import "./auth-evaluate.css";
|
||||
import { UserSelect } from "../../components/users/UserSelect";
|
||||
|
||||
interface EvaluateFormInputs
|
||||
extends Omit<ResourceEvaluation, "context" | "resources"> {
|
||||
|
@ -54,9 +45,9 @@ interface EvaluateFormInputs
|
|||
context: {
|
||||
attributes: Record<string, string>[];
|
||||
};
|
||||
resources: Record<string, string>[];
|
||||
resources?: Record<string, string>[];
|
||||
client: ClientRepresentation;
|
||||
user: UserRepresentation;
|
||||
user: string[];
|
||||
}
|
||||
|
||||
export type AttributeType = {
|
||||
|
@ -85,85 +76,40 @@ export type AttributeForm = Omit<
|
|||
|
||||
type Props = ClientSettingsProps & EvaluationResultRepresentation;
|
||||
|
||||
enum ResultsFilter {
|
||||
All = "ALL",
|
||||
StatusDenied = "STATUS_DENIED",
|
||||
StatusPermitted = "STATUS_PERMITTED",
|
||||
}
|
||||
|
||||
function filterResults(
|
||||
results: EvaluationResultRepresentation[],
|
||||
filter: ResultsFilter
|
||||
) {
|
||||
switch (filter) {
|
||||
case ResultsFilter.StatusPermitted:
|
||||
return results.filter(({ status }) => status === "PERMIT");
|
||||
case ResultsFilter.StatusDenied:
|
||||
return results.filter(({ status }) => status === "DENY");
|
||||
default:
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
export const AuthorizationEvaluate = ({ client }: Props) => {
|
||||
const form = useFormContext<EvaluateFormInputs>();
|
||||
const { control, reset, trigger } = form;
|
||||
const form = useForm<EvaluateFormInputs>({ mode: "onChange" });
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
reset,
|
||||
errors,
|
||||
trigger,
|
||||
formState: { isValid },
|
||||
} = form;
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const realm = useRealm();
|
||||
|
||||
const [clientsDropdownOpen, setClientsDropdownOpen] = useState(false);
|
||||
const [scopesDropdownOpen, setScopesDropdownOpen] = useState(false);
|
||||
|
||||
const [userDropdownOpen, setUserDropdownOpen] = useState(false);
|
||||
const [roleDropdownOpen, setRoleDropdownOpen] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [applyToResourceType, setApplyToResourceType] = useState(false);
|
||||
const [resources, setResources] = useState<ResourceRepresentation[]>([]);
|
||||
const [scopes, setScopes] = useState<ScopeRepresentation[]>([]);
|
||||
const [evaluateResults, setEvaluateResults] = useState<
|
||||
EvaluationResultRepresentation[]
|
||||
>([]);
|
||||
const [access, setAccess] = useState<AccessTokenRepresentation>();
|
||||
const [showEvaluateResults, setShowEvaluateResults] = useState(false);
|
||||
const searchQueryRef = useRef("");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false);
|
||||
const [key, setKey] = useState(0);
|
||||
const [evaluateResult, setEvaluateResult] =
|
||||
useState<PolicyEvaluationResponse>();
|
||||
|
||||
const refresh = () => {
|
||||
setKey(key + 1);
|
||||
};
|
||||
|
||||
const [filter, setFilter] = useState(ResultsFilter.All);
|
||||
|
||||
const [clients, setClients] = useState<ClientRepresentation[]>([]);
|
||||
const [clientRoles, setClientRoles] = useState<RoleRepresentation[]>([]);
|
||||
const [users, setUsers] = useState<UserRepresentation[]>([]);
|
||||
|
||||
const filteredResources = useMemo(
|
||||
() =>
|
||||
filterResults(evaluateResults, filter).filter(
|
||||
({ resource }) => resource?.name?.includes(searchQuery) ?? false
|
||||
),
|
||||
[evaluateResults, filter, searchQuery]
|
||||
);
|
||||
|
||||
const { hasAccess } = useAccess();
|
||||
if (!hasAccess("view-users"))
|
||||
return <ForbiddenSection permissionNeeded="view-users" />;
|
||||
|
||||
useFetch(
|
||||
() =>
|
||||
Promise.all([
|
||||
adminClient.clients.find(),
|
||||
adminClient.roles.find(),
|
||||
adminClient.users.find(),
|
||||
]),
|
||||
([clients, roles, users]) => {
|
||||
setClients(clients);
|
||||
() => adminClient.roles.find(),
|
||||
(roles) => {
|
||||
setClientRoles(roles);
|
||||
setUsers(users);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
@ -182,7 +128,7 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
|
|||
setResources(resources);
|
||||
setScopes(scopes);
|
||||
},
|
||||
[key, filter]
|
||||
[]
|
||||
);
|
||||
|
||||
const evaluate = async () => {
|
||||
|
@ -190,13 +136,13 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
|
|||
return;
|
||||
}
|
||||
const formValues = form.getValues();
|
||||
const keys = formValues.resources.map(({ key }) => key);
|
||||
const keys = formValues.resources?.map(({ key }) => key);
|
||||
const resEval: ResourceEvaluation = {
|
||||
roleIds: formValues.roleIds ?? [],
|
||||
clientId: formValues.client.id!,
|
||||
userId: formValues.user.id!,
|
||||
resources: formValues.resources.filter((resource) =>
|
||||
keys.includes(resource.name!)
|
||||
userId: formValues.user[0],
|
||||
resources: formValues.resources?.filter((resource) =>
|
||||
keys?.includes(resource.name!)
|
||||
),
|
||||
entitlements: false,
|
||||
context: {
|
||||
|
@ -213,442 +159,254 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
|
|||
resEval
|
||||
);
|
||||
|
||||
setEvaluateResults(evaluation.results);
|
||||
setAccess(evaluation.rpt);
|
||||
setShowEvaluateResults(true);
|
||||
return evaluateResults;
|
||||
setEvaluateResult(evaluation);
|
||||
return evaluation;
|
||||
};
|
||||
|
||||
const confirmSearchQuery = () => {
|
||||
setSearchQuery(searchQueryRef.current);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
confirmSearchQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputChange = (value: string) => {
|
||||
searchQueryRef.current = value;
|
||||
};
|
||||
|
||||
const noEvaluatedData = evaluateResults.length === 0;
|
||||
const noFilteredData = filteredResources.length === 0;
|
||||
|
||||
return showEvaluateResults ? (
|
||||
if (evaluateResult) {
|
||||
return (
|
||||
<Results
|
||||
evaluateResult={evaluateResult}
|
||||
refresh={evaluate}
|
||||
back={() => setEvaluateResult(undefined)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<PageSection>
|
||||
<Toolbar>
|
||||
<ToolbarGroup className="providers-toolbar">
|
||||
<ToolbarItem>
|
||||
<InputGroup>
|
||||
<TextInput
|
||||
name={"inputGroupName"}
|
||||
id={"inputGroupName"}
|
||||
type="search"
|
||||
aria-label={t("common:search")}
|
||||
placeholder={t("common:search")}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<Button
|
||||
variant={ButtonVariant.control}
|
||||
aria-label={t("common:search")}
|
||||
onClick={() => confirmSearchQuery()}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Select
|
||||
width={300}
|
||||
data-testid="filter-type-select"
|
||||
isOpen={filterDropdownOpen}
|
||||
className="kc-filter-type-select"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={() => setFilterDropdownOpen(!filterDropdownOpen)}
|
||||
onSelect={(_, value) => {
|
||||
setFilter(value as ResultsFilter);
|
||||
setFilterDropdownOpen(false);
|
||||
refresh();
|
||||
}}
|
||||
selections={filter}
|
||||
>
|
||||
<SelectOption
|
||||
data-testid="all-results-option"
|
||||
value={ResultsFilter.All}
|
||||
isPlaceholder
|
||||
>
|
||||
{t("allResults")}
|
||||
</SelectOption>
|
||||
<SelectOption
|
||||
data-testid="result-permit-option"
|
||||
value={ResultsFilter.StatusPermitted}
|
||||
>
|
||||
{t("resultPermit")}
|
||||
</SelectOption>
|
||||
<SelectOption
|
||||
data-testid="result-deny-option"
|
||||
value={ResultsFilter.StatusDenied}
|
||||
>
|
||||
{t("resultDeny")}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
</Toolbar>
|
||||
{!noFilteredData && (
|
||||
<TableComposable aria-label={t("evaluationResults")}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th>{t("resource")}</Th>
|
||||
<Th>{t("overallResults")}</Th>
|
||||
<Th>{t("scopes")}</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
{filteredResources.map((resource, rowIndex) => (
|
||||
<AuthorizationEvaluateResource
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex}
|
||||
resource={resource}
|
||||
evaluateResults={evaluateResults}
|
||||
/>
|
||||
))}
|
||||
</TableComposable>
|
||||
)}
|
||||
{(noFilteredData || noEvaluatedData) && (
|
||||
<>
|
||||
<Divider />
|
||||
<ListEmptyState
|
||||
isSearchVariant
|
||||
message={t("common:noSearchResults")}
|
||||
instructions={t("common:noSearchResultsInstructions")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ActionGroup className="kc-evaluated-options">
|
||||
<Button
|
||||
data-testid="authorization-eval"
|
||||
id="back-btn"
|
||||
onClick={() => setShowEvaluateResults(false)}
|
||||
<FormProvider {...form}>
|
||||
<FormPanel
|
||||
className="kc-identity-information"
|
||||
title={t("clients:identityInformation")}
|
||||
>
|
||||
{t("common:back")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="authorization-reevaluate"
|
||||
id="reevaluate-btn"
|
||||
variant="secondary"
|
||||
onClick={() => evaluate()}
|
||||
>
|
||||
{t("clients:reevaluate")}
|
||||
</Button>
|
||||
<AuthorizationDataModal data={access!} />
|
||||
</ActionGroup>
|
||||
</PageSection>
|
||||
) : (
|
||||
<PageSection>
|
||||
<FormPanel
|
||||
className="kc-identity-information"
|
||||
title={t("clients:identityInformation")}
|
||||
>
|
||||
<FormAccess
|
||||
isHorizontal
|
||||
role="view-clients"
|
||||
onSubmit={form.handleSubmit(evaluate)}
|
||||
>
|
||||
<FormGroup
|
||||
label={t("client")}
|
||||
isRequired
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:client"
|
||||
fieldLabelId="clients:client"
|
||||
/>
|
||||
}
|
||||
fieldId="client"
|
||||
>
|
||||
<Controller
|
||||
<FormAccess isHorizontal role="view-clients">
|
||||
<ClientSelect
|
||||
name="client"
|
||||
defaultValue={client}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="client"
|
||||
onToggle={setClientsDropdownOpen}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setClientsDropdownOpen(false);
|
||||
}}
|
||||
selections={value.clientId}
|
||||
variant={SelectVariant.typeahead}
|
||||
aria-label={t("client")}
|
||||
isOpen={clientsDropdownOpen}
|
||||
>
|
||||
{clients.map((client) => (
|
||||
<SelectOption
|
||||
selected={client.id === value.id}
|
||||
key={client.clientId}
|
||||
value={client}
|
||||
>
|
||||
{client.clientId}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
label="client"
|
||||
namespace="clients"
|
||||
helpText={"clients-help:client"}
|
||||
defaultValue={client.clientId}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("user")}
|
||||
isRequired
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:userSelect"
|
||||
fieldLabelId="clients:userSelect"
|
||||
/>
|
||||
}
|
||||
fieldId="user"
|
||||
>
|
||||
<Controller
|
||||
<UserSelect
|
||||
name="user"
|
||||
rules={{
|
||||
required: true,
|
||||
}}
|
||||
label="users"
|
||||
helpText="clients-help:selectUser"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="user"
|
||||
placeholderText={t("selectAUser")}
|
||||
onToggle={setUserDropdownOpen}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value);
|
||||
setUserDropdownOpen(false);
|
||||
}}
|
||||
selections={value.username}
|
||||
variant={SelectVariant.typeahead}
|
||||
aria-label={t("user")}
|
||||
isOpen={userDropdownOpen}
|
||||
>
|
||||
{users.map((user) => (
|
||||
<SelectOption
|
||||
selected={user.username === value.username}
|
||||
key={user.username}
|
||||
value={user}
|
||||
>
|
||||
{user.username}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("roles")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:roles"
|
||||
fieldLabelId="clients:roles"
|
||||
/>
|
||||
}
|
||||
fieldId="realmRole"
|
||||
>
|
||||
<Controller
|
||||
name="roleIds"
|
||||
placeholderText={t("selectARole")}
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
toggleId="role"
|
||||
onToggle={setRoleDropdownOpen}
|
||||
selections={value}
|
||||
onSelect={(_, v) => {
|
||||
const option = v.toString();
|
||||
if (value.includes(option)) {
|
||||
onChange(value.filter((item: string) => item !== option));
|
||||
} else {
|
||||
onChange([...value, option]);
|
||||
}
|
||||
setRoleDropdownOpen(false);
|
||||
}}
|
||||
onClear={(event) => {
|
||||
event.stopPropagation();
|
||||
onChange([]);
|
||||
}}
|
||||
aria-label={t("realmRole")}
|
||||
isOpen={roleDropdownOpen}
|
||||
>
|
||||
{clientRoles.map((role) => (
|
||||
<SelectOption
|
||||
selected={role.name === value}
|
||||
key={role.name}
|
||||
value={role.name}
|
||||
/>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormAccess>
|
||||
</FormPanel>
|
||||
<FormPanel className="kc-permissions" title={t("common:permissions")}>
|
||||
<FormAccess isHorizontal role="view-clients">
|
||||
<FormGroup
|
||||
label={t("applyToResourceType")}
|
||||
fieldId="applyToResourceType"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:applyToResourceType"
|
||||
fieldLabelId="clients:applyToResourceType"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
id="applyToResource-switch"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={applyToResourceType}
|
||||
onChange={setApplyToResourceType}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{!applyToResourceType ? (
|
||||
<FormGroup
|
||||
label={t("resourcesAndAuthScopes")}
|
||||
id="resourcesAndAuthScopes"
|
||||
variant={SelectVariant.typeahead}
|
||||
isRequired
|
||||
/>
|
||||
<FormGroup
|
||||
label={t("roles")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("clients-help:contextualAttributes")}
|
||||
fieldLabelId={`resourcesAndAuthScopes`}
|
||||
helpText="clients-help:roles"
|
||||
fieldLabelId="clients:roles"
|
||||
/>
|
||||
}
|
||||
fieldId="realmRole"
|
||||
validated={errors.roleIds ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
fieldId="resourcesAndAuthScopes"
|
||||
isRequired
|
||||
>
|
||||
<KeyBasedAttributeInput
|
||||
selectableValues={resources.map<AttributeType>((item) => ({
|
||||
name: item.name!,
|
||||
key: item._id!,
|
||||
}))}
|
||||
resources={resources}
|
||||
name="resources"
|
||||
<Controller
|
||||
name="roleIds"
|
||||
placeholderText={t("selectARole")}
|
||||
control={control}
|
||||
defaultValue={[]}
|
||||
rules={{ validate: (value) => value.length > 0 }}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
toggleId="role"
|
||||
onToggle={setRoleDropdownOpen}
|
||||
selections={value}
|
||||
onSelect={(_, v) => {
|
||||
const option = v.toString();
|
||||
if (value.includes(option)) {
|
||||
onChange(
|
||||
value.filter((item: string) => item !== option)
|
||||
);
|
||||
} else {
|
||||
onChange([...value, option]);
|
||||
}
|
||||
setRoleDropdownOpen(false);
|
||||
}}
|
||||
onClear={(event) => {
|
||||
event.stopPropagation();
|
||||
onChange([]);
|
||||
}}
|
||||
aria-label={t("realmRole")}
|
||||
isOpen={roleDropdownOpen}
|
||||
>
|
||||
{clientRoles.map((role) => (
|
||||
<SelectOption
|
||||
selected={role.name === value}
|
||||
key={role.name}
|
||||
value={role.name}
|
||||
/>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : (
|
||||
<>
|
||||
</FormAccess>
|
||||
</FormPanel>
|
||||
<FormPanel className="kc-permissions" title={t("common:permissions")}>
|
||||
<FormAccess isHorizontal role="view-clients">
|
||||
<FormGroup
|
||||
label={t("applyToResourceType")}
|
||||
fieldId="applyToResourceType"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:applyToResourceType"
|
||||
fieldLabelId="clients:applyToResourceType"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Switch
|
||||
id="applyToResource-switch"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
isChecked={applyToResourceType}
|
||||
onChange={setApplyToResourceType}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{!applyToResourceType ? (
|
||||
<FormGroup
|
||||
label={t("resourceType")}
|
||||
label={t("resourcesAndAuthScopes")}
|
||||
id="resourcesAndAuthScopes"
|
||||
isRequired
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:resourceType"
|
||||
fieldLabelId="clients:resourceType"
|
||||
helpText={t("clients-help:contextualAttributes")}
|
||||
fieldLabelId={`resourcesAndAuthScopes`}
|
||||
/>
|
||||
}
|
||||
fieldId="client"
|
||||
helperTextInvalid={t("common:required")}
|
||||
fieldId="resourcesAndAuthScopes"
|
||||
>
|
||||
<KeycloakTextInput
|
||||
type="text"
|
||||
id="alias"
|
||||
name="alias"
|
||||
data-testid="alias"
|
||||
ref={form.register({ required: true })}
|
||||
<KeyBasedAttributeInput
|
||||
selectableValues={resources.map<AttributeType>((item) => ({
|
||||
name: item.name!,
|
||||
key: item._id!,
|
||||
}))}
|
||||
resources={resources}
|
||||
name="resources"
|
||||
/>
|
||||
</FormGroup>
|
||||
) : (
|
||||
<>
|
||||
<FormGroup
|
||||
label={t("resourceType")}
|
||||
isRequired
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:resourceType"
|
||||
fieldLabelId="clients:resourceType"
|
||||
/>
|
||||
}
|
||||
fieldId="client"
|
||||
validated={form.errors.alias ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
type="text"
|
||||
id="alias"
|
||||
name="alias"
|
||||
data-testid="alias"
|
||||
ref={register({ required: true })}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("authScopes")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:scopesSelect"
|
||||
fieldLabelId="clients:client"
|
||||
/>
|
||||
}
|
||||
fieldId="authScopes"
|
||||
>
|
||||
<Controller
|
||||
name="authScopes"
|
||||
defaultValue={[]}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="authScopes"
|
||||
onToggle={setScopesDropdownOpen}
|
||||
onSelect={(_, v) => {
|
||||
const option = v.toString();
|
||||
if (value.includes(option)) {
|
||||
onChange(
|
||||
value.filter((item: string) => item !== option)
|
||||
);
|
||||
} else {
|
||||
onChange([...value, option]);
|
||||
}
|
||||
setScopesDropdownOpen(false);
|
||||
}}
|
||||
selections={value}
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
aria-label={t("authScopes")}
|
||||
isOpen={scopesDropdownOpen}
|
||||
>
|
||||
{scopes.map((scope) => (
|
||||
<SelectOption
|
||||
selected={scope.name === value}
|
||||
key={scope.id}
|
||||
value={scope.name}
|
||||
/>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
<ExpandableSection
|
||||
toggleText={t("contextualInfo")}
|
||||
onToggle={() => setIsExpanded(!isExpanded)}
|
||||
isExpanded={isExpanded}
|
||||
>
|
||||
<FormGroup
|
||||
label={t("authScopes")}
|
||||
label={t("contextualAttributes")}
|
||||
id="contextualAttributes"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:scopesSelect"
|
||||
fieldLabelId="clients:client"
|
||||
helpText={t("clients-help:contextualAttributes")}
|
||||
fieldLabelId={`contextualAttributes`}
|
||||
/>
|
||||
}
|
||||
fieldId="authScopes"
|
||||
helperTextInvalid={t("common:required")}
|
||||
fieldId="contextualAttributes"
|
||||
>
|
||||
<Controller
|
||||
name="authScopes"
|
||||
defaultValue={[]}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="authScopes"
|
||||
onToggle={setScopesDropdownOpen}
|
||||
onSelect={(_, v) => {
|
||||
const option = v.toString();
|
||||
if (value.includes(option)) {
|
||||
onChange(
|
||||
value.filter((item: string) => item !== option)
|
||||
);
|
||||
} else {
|
||||
onChange([...value, option]);
|
||||
}
|
||||
setScopesDropdownOpen(false);
|
||||
}}
|
||||
selections={value}
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
aria-label={t("authScopes")}
|
||||
isOpen={scopesDropdownOpen}
|
||||
>
|
||||
{scopes.map((scope) => (
|
||||
<SelectOption
|
||||
selected={scope.name === value}
|
||||
key={scope.id}
|
||||
value={scope.name}
|
||||
/>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
<KeyBasedAttributeInput
|
||||
selectableValues={defaultContextAttributes}
|
||||
name="context.attributes"
|
||||
/>
|
||||
</FormGroup>
|
||||
</>
|
||||
)}
|
||||
<ExpandableSection
|
||||
toggleText={t("contextualInfo")}
|
||||
onToggle={() => setIsExpanded(!isExpanded)}
|
||||
isExpanded={isExpanded}
|
||||
>
|
||||
<FormGroup
|
||||
label={t("contextualAttributes")}
|
||||
id="contextualAttributes"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("clients-help:contextualAttributes")}
|
||||
fieldLabelId={`contextualAttributes`}
|
||||
/>
|
||||
}
|
||||
helperTextInvalid={t("common:required")}
|
||||
fieldId="contextualAttributes"
|
||||
</ExpandableSection>
|
||||
</FormAccess>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
data-testid="authorization-eval"
|
||||
isDisabled={!isValid}
|
||||
onClick={() => evaluate()}
|
||||
>
|
||||
<KeyBasedAttributeInput
|
||||
selectableValues={defaultContextAttributes}
|
||||
name="context.attributes"
|
||||
/>
|
||||
</FormGroup>
|
||||
</ExpandableSection>
|
||||
</FormAccess>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
data-testid="authorization-eval"
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
isDisabled={form.getValues().resources?.every((e) => e.key === "")}
|
||||
onClick={() => evaluate()}
|
||||
>
|
||||
{t("evaluate")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="authorization-revert"
|
||||
variant="link"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
{t("common:revert")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormPanel>
|
||||
{t("evaluate")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="authorization-revert"
|
||||
variant="link"
|
||||
onClick={() => reset()}
|
||||
>
|
||||
{t("common:revert")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormPanel>
|
||||
</FormProvider>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
||||
|
|
193
src/clients/authorization/evaluate/Results.tsx
Normal file
193
src/clients/authorization/evaluate/Results.tsx
Normal file
|
@ -0,0 +1,193 @@
|
|||
import React, { KeyboardEvent, useMemo, useState } from "react";
|
||||
import {
|
||||
Select,
|
||||
SelectVariant,
|
||||
SelectOption,
|
||||
PageSection,
|
||||
ActionGroup,
|
||||
Button,
|
||||
TextInput,
|
||||
ButtonVariant,
|
||||
InputGroup,
|
||||
Toolbar,
|
||||
ToolbarGroup,
|
||||
ToolbarItem,
|
||||
Divider,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SearchIcon } from "@patternfly/react-icons";
|
||||
import { TableComposable, Th, Thead, Tr } from "@patternfly/react-table";
|
||||
|
||||
import type EvaluationResultRepresentation from "@keycloak/keycloak-admin-client/lib/defs/evaluationResultRepresentation";
|
||||
import type PolicyEvaluationResponse from "@keycloak/keycloak-admin-client/lib/defs/policyEvaluationResponse";
|
||||
import { AuthorizationEvaluateResource } from "../AuthorizationEvaluateResource";
|
||||
import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState";
|
||||
import { AuthorizationDataModal } from "../AuthorizationDataModal";
|
||||
import useToggle from "../../../utils/useToggle";
|
||||
|
||||
type ResultProps = {
|
||||
evaluateResult: PolicyEvaluationResponse;
|
||||
refresh: () => void;
|
||||
back: () => void;
|
||||
};
|
||||
|
||||
enum ResultsFilter {
|
||||
All = "ALL",
|
||||
StatusDenied = "STATUS_DENIED",
|
||||
StatusPermitted = "STATUS_PERMITTED",
|
||||
}
|
||||
|
||||
function filterResults(
|
||||
results: EvaluationResultRepresentation[],
|
||||
filter: ResultsFilter
|
||||
) {
|
||||
switch (filter) {
|
||||
case ResultsFilter.StatusPermitted:
|
||||
return results.filter(({ status }) => status === "PERMIT");
|
||||
case ResultsFilter.StatusDenied:
|
||||
return results.filter(({ status }) => status === "DENY");
|
||||
default:
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
export const Results = ({ evaluateResult, refresh, back }: ResultProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
|
||||
const [filterDropdownOpen, toggleFilterDropdown] = useToggle();
|
||||
|
||||
const [filter, setFilter] = useState(ResultsFilter.All);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
|
||||
const confirmSearchQuery = () => {
|
||||
setSearchQuery(searchInput);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === "Enter") {
|
||||
confirmSearchQuery();
|
||||
}
|
||||
};
|
||||
|
||||
const filteredResources = useMemo(
|
||||
() =>
|
||||
filterResults(evaluateResult.results!, filter).filter(
|
||||
({ resource }) => resource?.name?.includes(searchQuery) ?? false
|
||||
),
|
||||
[evaluateResult.results, filter, searchQuery]
|
||||
);
|
||||
|
||||
const noEvaluatedData = evaluateResult.results!.length === 0;
|
||||
const noFilteredData = filteredResources.length === 0;
|
||||
|
||||
return (
|
||||
<PageSection>
|
||||
<Toolbar>
|
||||
<ToolbarGroup className="providers-toolbar">
|
||||
<ToolbarItem>
|
||||
<InputGroup>
|
||||
<TextInput
|
||||
name={"inputGroupName"}
|
||||
id={"inputGroupName"}
|
||||
type="search"
|
||||
aria-label={t("common:search")}
|
||||
placeholder={t("common:search")}
|
||||
onChange={setSearchInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
<Button
|
||||
variant={ButtonVariant.control}
|
||||
aria-label={t("common:search")}
|
||||
onClick={() => confirmSearchQuery()}
|
||||
>
|
||||
<SearchIcon />
|
||||
</Button>
|
||||
</InputGroup>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Select
|
||||
width={300}
|
||||
data-testid="filter-type-select"
|
||||
isOpen={filterDropdownOpen}
|
||||
className="kc-filter-type-select"
|
||||
variant={SelectVariant.single}
|
||||
onToggle={toggleFilterDropdown}
|
||||
onSelect={(_, value) => {
|
||||
setFilter(value as ResultsFilter);
|
||||
toggleFilterDropdown();
|
||||
refresh();
|
||||
}}
|
||||
selections={filter}
|
||||
>
|
||||
<SelectOption
|
||||
data-testid="all-results-option"
|
||||
value={ResultsFilter.All}
|
||||
isPlaceholder
|
||||
>
|
||||
{t("allResults")}
|
||||
</SelectOption>
|
||||
<SelectOption
|
||||
data-testid="result-permit-option"
|
||||
value={ResultsFilter.StatusPermitted}
|
||||
>
|
||||
{t("resultPermit")}
|
||||
</SelectOption>
|
||||
<SelectOption
|
||||
data-testid="result-deny-option"
|
||||
value={ResultsFilter.StatusDenied}
|
||||
>
|
||||
{t("resultDeny")}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</ToolbarItem>
|
||||
</ToolbarGroup>
|
||||
</Toolbar>
|
||||
{!noFilteredData && (
|
||||
<TableComposable aria-label={t("evaluationResults")}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th />
|
||||
<Th>{t("resource")}</Th>
|
||||
<Th>{t("overallResults")}</Th>
|
||||
<Th>{t("scopes")}</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
{filteredResources.map((resource, rowIndex) => (
|
||||
<AuthorizationEvaluateResource
|
||||
key={rowIndex}
|
||||
rowIndex={rowIndex}
|
||||
resource={resource}
|
||||
evaluateResults={evaluateResult.results}
|
||||
/>
|
||||
))}
|
||||
</TableComposable>
|
||||
)}
|
||||
{(noFilteredData || noEvaluatedData) && (
|
||||
<>
|
||||
<Divider />
|
||||
<ListEmptyState
|
||||
isSearchVariant
|
||||
message={t("common:noSearchResults")}
|
||||
instructions={t("common:noSearchResultsInstructions")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<ActionGroup className="kc-evaluated-options">
|
||||
<Button data-testid="authorization-eval" id="back-btn" onClick={back}>
|
||||
{t("common:back")}
|
||||
</Button>
|
||||
<Button
|
||||
data-testid="authorization-reevaluate"
|
||||
id="reevaluate-btn"
|
||||
variant="secondary"
|
||||
onClick={refresh}
|
||||
>
|
||||
{t("clients:reevaluate")}
|
||||
</Button>
|
||||
<AuthorizationDataModal data={evaluateResult.rpt!} />
|
||||
</ActionGroup>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
|
@ -1,114 +1,13 @@
|
|||
import React, { useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import {
|
||||
SelectOption,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectVariant,
|
||||
} from "@patternfly/react-core";
|
||||
import React from "react";
|
||||
|
||||
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
import type { UserQuery } from "@keycloak/keycloak-admin-client/lib/resources/users";
|
||||
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserSelect } from "../../../components/users/UserSelect";
|
||||
|
||||
export const User = () => {
|
||||
const { t } = useTranslation("clients");
|
||||
const {
|
||||
control,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
const values: string[] | undefined = getValues("users");
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [users, setUsers] = useState<UserRepresentation[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
const params: UserQuery = {
|
||||
max: 20,
|
||||
};
|
||||
if (search) {
|
||||
params.name = search;
|
||||
}
|
||||
|
||||
if (values?.length && !search) {
|
||||
return await Promise.all(
|
||||
values.map(
|
||||
(id: string) =>
|
||||
adminClient.users.findOne({ id }) as UserRepresentation
|
||||
)
|
||||
);
|
||||
}
|
||||
return await adminClient.users.find(params);
|
||||
},
|
||||
setUsers,
|
||||
[search]
|
||||
);
|
||||
|
||||
const convert = (clients: UserRepresentation[]) =>
|
||||
clients.map((option) => (
|
||||
<SelectOption
|
||||
key={option.id!}
|
||||
value={option.id}
|
||||
selected={values?.includes(option.id!)}
|
||||
>
|
||||
{option.username}
|
||||
</SelectOption>
|
||||
));
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={t("users")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:policyUsers"
|
||||
fieldLabelId="clients:users"
|
||||
/>
|
||||
}
|
||||
fieldId="users"
|
||||
helperTextInvalid={t("common:required")}
|
||||
validated={errors.users ? "error" : "default"}
|
||||
isRequired
|
||||
>
|
||||
<Controller
|
||||
name="users"
|
||||
defaultValue={[]}
|
||||
control={control}
|
||||
rules={{
|
||||
validate: (value) => value.length > 0,
|
||||
}}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="users"
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
onToggle={(open) => setOpen(open)}
|
||||
isOpen={open}
|
||||
selections={value}
|
||||
onFilter={(_, value) => {
|
||||
setSearch(value);
|
||||
return convert(users);
|
||||
}}
|
||||
onSelect={(_, v) => {
|
||||
const option = v.toString();
|
||||
if (value.includes(option)) {
|
||||
onChange(value.filter((item: string) => item !== option));
|
||||
} else {
|
||||
onChange([...value, option]);
|
||||
}
|
||||
setOpen(false);
|
||||
}}
|
||||
aria-label={t("users")}
|
||||
>
|
||||
{convert(users)}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
export const User = () => (
|
||||
<UserSelect
|
||||
name="users"
|
||||
label="users"
|
||||
helpText="clients-help:policyUsers"
|
||||
defaultValue={[]}
|
||||
isRequired
|
||||
/>
|
||||
);
|
||||
|
|
103
src/components/client/ClientSelect.tsx
Normal file
103
src/components/client/ClientSelect.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
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 type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||
import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients";
|
||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||
import { HelpItem } from "../help-enabler/HelpItem";
|
||||
import type { ComponentProps } from "../dynamic/components";
|
||||
|
||||
type ClientSelectProps = ComponentProps & {
|
||||
namespace: string;
|
||||
required?: boolean;
|
||||
};
|
||||
|
||||
export const ClientSelect = ({
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
defaultValue,
|
||||
namespace,
|
||||
isDisabled = false,
|
||||
required = false,
|
||||
}: ClientSelectProps) => {
|
||||
const { t } = useTranslation(namespace);
|
||||
const { control, errors } = useFormContext();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [clients, setClients] = useState<ClientRepresentation[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
|
||||
useFetch(
|
||||
() => {
|
||||
const params: ClientQuery = {
|
||||
max: 20,
|
||||
};
|
||||
if (search) {
|
||||
params.clientId = search;
|
||||
params.search = true;
|
||||
}
|
||||
return adminClient.clients.find(params);
|
||||
},
|
||||
(clients) => setClients(clients),
|
||||
[search]
|
||||
);
|
||||
|
||||
const convert = (clients: ClientRepresentation[]) =>
|
||||
clients.map((option) => (
|
||||
<SelectOption key={option.id} value={option.clientId} />
|
||||
));
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={t(label!)}
|
||||
isRequired={required}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t(helpText!)}
|
||||
fieldLabelId={`${namespace}:${label}`}
|
||||
/>
|
||||
}
|
||||
fieldId={name!}
|
||||
validated={errors[name!] ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<Controller
|
||||
name={name!}
|
||||
defaultValue={defaultValue || ""}
|
||||
control={control}
|
||||
rules={required ? { required: true } : {}}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId={name}
|
||||
variant={SelectVariant.typeahead}
|
||||
onToggle={(open) => setOpen(open)}
|
||||
isOpen={open}
|
||||
isDisabled={isDisabled}
|
||||
selections={value}
|
||||
onFilter={(_, value) => {
|
||||
setSearch(value);
|
||||
return convert(clients);
|
||||
}}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value.toString());
|
||||
setOpen(false);
|
||||
}}
|
||||
aria-label={t(label!)}
|
||||
>
|
||||
{convert(clients)}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
|
@ -1,88 +1,14 @@
|
|||
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 React from "react";
|
||||
|
||||
import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients";
|
||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||
import { HelpItem } from "../help-enabler/HelpItem";
|
||||
import type { ComponentProps } from "./components";
|
||||
import { ClientSelect } from "../client/ClientSelect";
|
||||
|
||||
export const ClientSelectComponent = ({
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
defaultValue,
|
||||
isDisabled = false,
|
||||
}: ComponentProps) => {
|
||||
const { t } = useTranslation("dynamic");
|
||||
const { control } = useFormContext();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [clients, setClients] = useState<JSX.Element[]>();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
|
||||
useFetch(
|
||||
() => {
|
||||
const params: ClientQuery = {
|
||||
max: 20,
|
||||
};
|
||||
if (search) {
|
||||
params.clientId = search;
|
||||
params.search = true;
|
||||
}
|
||||
return adminClient.clients.find(params);
|
||||
},
|
||||
(clients) =>
|
||||
setClients(
|
||||
clients.map((option) => (
|
||||
<SelectOption key={option.id} value={option.clientId} />
|
||||
))
|
||||
),
|
||||
[search]
|
||||
);
|
||||
|
||||
export const ClientSelectComponent = (props: ComponentProps) => {
|
||||
return (
|
||||
<FormGroup
|
||||
label={t(label!)}
|
||||
labelIcon={
|
||||
<HelpItem helpText={t(helpText!)} fieldLabelId={`dynamic:${label}`} />
|
||||
}
|
||||
fieldId={name!}
|
||||
>
|
||||
<Controller
|
||||
name={`config.${name}`}
|
||||
defaultValue={defaultValue || ""}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId={name}
|
||||
variant={SelectVariant.typeahead}
|
||||
onToggle={(open) => setOpen(open)}
|
||||
isOpen={open}
|
||||
isDisabled={isDisabled}
|
||||
selections={value}
|
||||
onFilter={(_, value) => {
|
||||
setSearch(value);
|
||||
return clients;
|
||||
}}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value.toString());
|
||||
setOpen(false);
|
||||
}}
|
||||
aria-label={t(label!)}
|
||||
>
|
||||
{clients}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ClientSelect
|
||||
{...props}
|
||||
name={`config.${props.name}`}
|
||||
namespace="dynamic"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
130
src/components/users/UserSelect.tsx
Normal file
130
src/components/users/UserSelect.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import {
|
||||
SelectOption,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectVariant,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
import type { UserQuery } from "@keycloak/keycloak-admin-client/lib/resources/users";
|
||||
import type { ComponentProps } from "../dynamic/components";
|
||||
|
||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||
import { HelpItem } from "../help-enabler/HelpItem";
|
||||
import useToggle from "../../utils/useToggle";
|
||||
|
||||
type UserSelectProps = ComponentProps & {
|
||||
variant?: SelectVariant;
|
||||
isRequired?: boolean;
|
||||
};
|
||||
|
||||
export const UserSelect = ({
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
defaultValue,
|
||||
isRequired,
|
||||
variant = SelectVariant.typeaheadMulti,
|
||||
}: UserSelectProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const {
|
||||
control,
|
||||
getValues,
|
||||
formState: { errors },
|
||||
} = useFormContext();
|
||||
const values: string[] | undefined = getValues(name!);
|
||||
|
||||
const [open, toggleOpen] = useToggle();
|
||||
const [users, setUsers] = useState<(UserRepresentation | undefined)[]>([]);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
|
||||
useFetch(
|
||||
() => {
|
||||
const params: UserQuery = {
|
||||
max: 20,
|
||||
};
|
||||
if (search) {
|
||||
params.name = search;
|
||||
}
|
||||
|
||||
if (values?.length && !search) {
|
||||
return Promise.all(
|
||||
values.map((id: string) => adminClient.users.findOne({ id }))
|
||||
);
|
||||
}
|
||||
return adminClient.users.find(params);
|
||||
},
|
||||
setUsers,
|
||||
[search]
|
||||
);
|
||||
|
||||
const convert = (clients: (UserRepresentation | undefined)[]) =>
|
||||
clients
|
||||
.filter((c) => c !== undefined)
|
||||
.map((option) => (
|
||||
<SelectOption
|
||||
key={option!.id}
|
||||
value={option!.id}
|
||||
selected={values?.includes(option!.id!)}
|
||||
>
|
||||
{option!.username}
|
||||
</SelectOption>
|
||||
));
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={t(label!)}
|
||||
isRequired={isRequired}
|
||||
labelIcon={
|
||||
<HelpItem helpText={helpText!} fieldLabelId={`clients:${label}`} />
|
||||
}
|
||||
fieldId={name!}
|
||||
validated={errors[name!] ? "error" : "default"}
|
||||
helperTextInvalid={t("common:required")}
|
||||
>
|
||||
<Controller
|
||||
name={name!}
|
||||
defaultValue={defaultValue}
|
||||
control={control}
|
||||
rules={
|
||||
isRequired && variant === SelectVariant.typeaheadMulti
|
||||
? { validate: (value) => value.length > 0 }
|
||||
: isRequired
|
||||
? { required: true }
|
||||
: {}
|
||||
}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId={name!}
|
||||
variant={variant}
|
||||
placeholderText={t("selectAUser")}
|
||||
onToggle={toggleOpen}
|
||||
isOpen={open}
|
||||
selections={value}
|
||||
onFilter={(_, value) => {
|
||||
setSearch(value);
|
||||
return convert(users);
|
||||
}}
|
||||
onSelect={(_, v) => {
|
||||
const option = v.toString();
|
||||
if (value.includes(option)) {
|
||||
onChange(value.filter((item: string) => item !== option));
|
||||
} else {
|
||||
onChange([...value, option]);
|
||||
}
|
||||
toggleOpen();
|
||||
}}
|
||||
aria-label={t(name!)}
|
||||
>
|
||||
{convert(users)}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
Loading…
Reference in a new issue