Make filtering of client scopes same for clients and client scope section (#1064)
Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
71d881ccd0
commit
b8990cbf63
11 changed files with 421 additions and 245 deletions
|
@ -1,67 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Form,
|
||||
Modal,
|
||||
Radio,
|
||||
} from "@patternfly/react-core";
|
||||
import {
|
||||
AllClientScopes,
|
||||
AllClientScopeType,
|
||||
allClientScopeTypes,
|
||||
} from "../components/client-scope/ClientScopeTypes";
|
||||
|
||||
type ChangeTypeDialogProps = {
|
||||
selectedClientScopes: number;
|
||||
onConfirm: (scope: AllClientScopeType) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const ChangeTypeDialog = ({
|
||||
selectedClientScopes,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: ChangeTypeDialogProps) => {
|
||||
const { t } = useTranslation("client-scopes");
|
||||
const [value, setValue] = useState<AllClientScopeType>(AllClientScopes.none);
|
||||
return (
|
||||
<Modal
|
||||
title={t("changeType")}
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
variant="small"
|
||||
description={t("changeTypeIntro", { count: selectedClientScopes })}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="change-scope-dialog-confirm"
|
||||
key="confirm"
|
||||
onClick={() => onConfirm(value)}
|
||||
>
|
||||
{t("common:continue")}
|
||||
</Button>,
|
||||
<Button key="cancel" variant={ButtonVariant.link} onClick={onClose}>
|
||||
{t("common:cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form isHorizontal>
|
||||
{allClientScopeTypes.map((scope) => (
|
||||
<Radio
|
||||
key={scope}
|
||||
isChecked={scope === value}
|
||||
name={`radio-${scope}`}
|
||||
onChange={(_val, event) => {
|
||||
const { value } = event.currentTarget;
|
||||
setValue(value as AllClientScopeType);
|
||||
}}
|
||||
label={t(`common:clientScope.${scope}`)}
|
||||
id={`radio-${scope}`}
|
||||
value={scope}
|
||||
/>
|
||||
))}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
70
src/client-scopes/ChangeTypeDropdown.tsx
Normal file
70
src/client-scopes/ChangeTypeDropdown.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AlertVariant, Select } from "@patternfly/react-core";
|
||||
|
||||
import {
|
||||
allClientScopeTypes,
|
||||
changeClientScope,
|
||||
changeScope,
|
||||
ClientScope,
|
||||
clientScopeTypesSelectOptions,
|
||||
} from "../components/client-scope/ClientScopeTypes";
|
||||
import type { Row } from "../clients/scopes/ClientScopes";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
|
||||
type ChangeTypeDropdownProps = {
|
||||
clientId?: string;
|
||||
selectedRows: Row[];
|
||||
refresh: () => void;
|
||||
};
|
||||
|
||||
export const ChangeTypeDropdown = ({
|
||||
clientId,
|
||||
selectedRows,
|
||||
refresh,
|
||||
}: ChangeTypeDropdownProps) => {
|
||||
const { t } = useTranslation("client-scopes");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
return (
|
||||
<Select
|
||||
id="change-type-dropdown"
|
||||
isOpen={open}
|
||||
selections={[]}
|
||||
isDisabled={selectedRows.length === 0}
|
||||
placeholderText={t("changeTypeTo")}
|
||||
onToggle={(isExpanded) => setOpen(isExpanded)}
|
||||
onSelect={async (_, value) => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map((row) => {
|
||||
return clientId
|
||||
? changeClientScope(
|
||||
adminClient,
|
||||
clientId,
|
||||
row,
|
||||
row.type,
|
||||
value as ClientScope
|
||||
)
|
||||
: changeScope(adminClient, row, value as ClientScope);
|
||||
})
|
||||
);
|
||||
setOpen(false);
|
||||
refresh();
|
||||
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError("clients:clientScopeError", error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{clientScopeTypesSelectOptions(
|
||||
t,
|
||||
!clientId ? allClientScopeTypes : undefined
|
||||
)}
|
||||
</Select>
|
||||
);
|
||||
};
|
|
@ -28,12 +28,22 @@ import {
|
|||
changeScope,
|
||||
removeScope,
|
||||
} from "../components/client-scope/ClientScopeTypes";
|
||||
import { ChangeTypeDialog } from "./ChangeTypeDialog";
|
||||
import { ChangeTypeDropdown } from "./ChangeTypeDropdown";
|
||||
import { toNewClientScope } from "./routes/NewClientScope";
|
||||
|
||||
import "./client-scope.css";
|
||||
import { toClientScope } from "./routes/ClientScope";
|
||||
import { useWhoAmI } from "../context/whoami/WhoAmI";
|
||||
import {
|
||||
nameFilter,
|
||||
protocolFilter,
|
||||
ProtocolType,
|
||||
SearchDropdown,
|
||||
SearchToolbar,
|
||||
SearchType,
|
||||
typeFilter,
|
||||
} from "./details/SearchFilter";
|
||||
import type { Row } from "../clients/scopes/ClientScopes";
|
||||
|
||||
export const ClientScopesSection = () => {
|
||||
const { realm } = useRealm();
|
||||
|
@ -47,21 +57,33 @@ export const ClientScopesSection = () => {
|
|||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const [kebabOpen, setKebabOpen] = useState(false);
|
||||
const [changeTypeOpen, setChangeTypeOpen] = useState(false);
|
||||
const [selectedScopes, setSelectedScopes] = useState<
|
||||
ClientScopeDefaultOptionalType[]
|
||||
>([]);
|
||||
|
||||
const loader = async () => {
|
||||
const [searchType, setSearchType] = useState<SearchType>("name");
|
||||
const [searchTypeType, setSearchTypeType] = useState<AllClientScopes>(
|
||||
AllClientScopes.none
|
||||
);
|
||||
const [searchProtocol, setSearchProtocol] = useState<ProtocolType>("all");
|
||||
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
const defaultScopes =
|
||||
await adminClient.clientScopes.listDefaultClientScopes();
|
||||
const optionalScopes =
|
||||
await adminClient.clientScopes.listDefaultOptionalClientScopes();
|
||||
const clientScopes = await adminClient.clientScopes.find();
|
||||
|
||||
const filter =
|
||||
searchType === "name"
|
||||
? nameFilter(search)
|
||||
: searchType === "type"
|
||||
? typeFilter(searchTypeType)
|
||||
: protocolFilter(searchProtocol);
|
||||
|
||||
return clientScopes
|
||||
.map((scope) => {
|
||||
return {
|
||||
const row: Row = {
|
||||
...scope,
|
||||
type: defaultScopes.find(
|
||||
(defaultScope) => defaultScope.name === scope.name
|
||||
|
@ -73,8 +95,11 @@ export const ClientScopesSection = () => {
|
|||
? ClientScope.optional
|
||||
: AllClientScopes.none,
|
||||
};
|
||||
return row;
|
||||
})
|
||||
.sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale()));
|
||||
.filter(filter)
|
||||
.sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale()))
|
||||
.slice(first, max);
|
||||
};
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
|
@ -138,24 +163,6 @@ export const ClientScopesSection = () => {
|
|||
return (
|
||||
<>
|
||||
<DeleteConfirm />
|
||||
{changeTypeOpen && (
|
||||
<ChangeTypeDialog
|
||||
selectedClientScopes={selectedScopes.length}
|
||||
onConfirm={(type) => {
|
||||
selectedScopes.map(async (scope) => {
|
||||
try {
|
||||
await changeScope(adminClient, scope, type);
|
||||
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addError("client-scopes:clientScopeError", error);
|
||||
}
|
||||
});
|
||||
setChangeTypeOpen(false);
|
||||
}}
|
||||
onClose={() => setChangeTypeOpen(false)}
|
||||
/>
|
||||
)}
|
||||
<ViewHeader
|
||||
titleKey="clientScopes"
|
||||
subKey="client-scopes:clientScopeExplain"
|
||||
|
@ -165,17 +172,49 @@ export const ClientScopesSection = () => {
|
|||
key={key}
|
||||
loader={loader}
|
||||
ariaLabelKey="client-scopes:clientScopeList"
|
||||
searchPlaceholderKey="client-scopes:searchFor"
|
||||
searchPlaceholderKey={
|
||||
searchType === "name" ? "client-scopes:searchFor" : undefined
|
||||
}
|
||||
isSearching={searchType !== "name"}
|
||||
searchTypeComponent={
|
||||
<SearchDropdown
|
||||
searchType={searchType}
|
||||
onSelect={(searchType) => setSearchType(searchType)}
|
||||
withProtocol
|
||||
/>
|
||||
}
|
||||
isPaginated
|
||||
onSelect={(clientScopes) => setSelectedScopes([...clientScopes])}
|
||||
canSelectAll
|
||||
toolbarItem={
|
||||
<>
|
||||
<SearchToolbar
|
||||
searchType={searchType}
|
||||
type={searchTypeType}
|
||||
onSelect={(searchType) => setSearchType(searchType)}
|
||||
onType={(value) => {
|
||||
setSearchTypeType(value);
|
||||
refresh();
|
||||
}}
|
||||
protocol={searchProtocol}
|
||||
onProtocol={(protocol) => {
|
||||
setSearchProtocol(protocol);
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
|
||||
<ToolbarItem>
|
||||
{/* @ts-ignore */}
|
||||
<Button component={Link} to={toNewClientScope({ realm })}>
|
||||
{t("createClientScope")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<ChangeTypeDropdown
|
||||
selectedRows={selectedScopes}
|
||||
refresh={refresh}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Dropdown
|
||||
toggle={
|
||||
|
@ -184,18 +223,6 @@ export const ClientScopesSection = () => {
|
|||
isOpen={kebabOpen}
|
||||
isPlain
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key="changeType"
|
||||
component="button"
|
||||
isDisabled={selectedScopes.length === 0}
|
||||
onClick={() => {
|
||||
setChangeTypeOpen(true);
|
||||
setKebabOpen(false);
|
||||
}}
|
||||
>
|
||||
{t("changeType")}
|
||||
</DropdownItem>,
|
||||
|
||||
<DropdownItem
|
||||
key="action"
|
||||
component="button"
|
||||
|
|
164
src/client-scopes/details/SearchFilter.tsx
Normal file
164
src/client-scopes/details/SearchFilter.tsx
Normal file
|
@ -0,0 +1,164 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownToggle,
|
||||
Select,
|
||||
SelectOption,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
import { FilterIcon } from "@patternfly/react-icons";
|
||||
|
||||
import {
|
||||
AllClientScopes,
|
||||
AllClientScopeType,
|
||||
clientScopeTypesSelectOptions,
|
||||
} from "../../components/client-scope/ClientScopeTypes";
|
||||
import type { Row } from "../../clients/scopes/ClientScopes";
|
||||
|
||||
export type SearchType = "name" | "type" | "protocol";
|
||||
export const PROTOCOLS = ["all", "saml", "openid-connect"] as const;
|
||||
export type ProtocolType = typeof PROTOCOLS[number];
|
||||
|
||||
export const nameFilter =
|
||||
(search = "") =>
|
||||
(scope: Row) =>
|
||||
scope.name?.includes(search);
|
||||
export const typeFilter = (type: AllClientScopeType) => (scope: Row) =>
|
||||
type === AllClientScopes.none || scope.type === type;
|
||||
|
||||
export const protocolFilter = (protocol: ProtocolType) => (scope: Row) =>
|
||||
protocol === "all" || scope.protocol === protocol;
|
||||
|
||||
type SearchToolbarProps = Omit<SearchDropdownProps, "withProtocol"> & {
|
||||
type: AllClientScopeType;
|
||||
onType: (value: AllClientScopes) => void;
|
||||
protocol?: ProtocolType;
|
||||
onProtocol?: (value: ProtocolType) => void;
|
||||
};
|
||||
|
||||
type SearchDropdownProps = {
|
||||
searchType: SearchType;
|
||||
onSelect: (value: SearchType) => void;
|
||||
withProtocol?: boolean;
|
||||
};
|
||||
|
||||
export const SearchDropdown = ({
|
||||
searchType,
|
||||
withProtocol = false,
|
||||
onSelect,
|
||||
}: SearchDropdownProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const [searchToggle, setSearchToggle] = useState(false);
|
||||
|
||||
const createDropdown = (searchType: SearchType) => (
|
||||
<DropdownItem
|
||||
key={searchType}
|
||||
onClick={() => {
|
||||
onSelect(searchType);
|
||||
setSearchToggle(false);
|
||||
}}
|
||||
>
|
||||
{t(`clientScopeSearch.${searchType}`)}
|
||||
</DropdownItem>
|
||||
);
|
||||
const options = [createDropdown("name"), createDropdown("type")];
|
||||
if (withProtocol) {
|
||||
options.push(createDropdown("protocol"));
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="keycloak__client-scopes__searchtype"
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
id="toggle-id"
|
||||
onToggle={(open) => setSearchToggle(open)}
|
||||
>
|
||||
<FilterIcon /> {t(`clientScopeSearch.${searchType}`)}
|
||||
</DropdownToggle>
|
||||
}
|
||||
isOpen={searchToggle}
|
||||
dropdownItems={options}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SearchToolbar = ({
|
||||
searchType,
|
||||
onSelect,
|
||||
type,
|
||||
onType,
|
||||
protocol,
|
||||
onProtocol,
|
||||
}: SearchToolbarProps) => {
|
||||
const { t } = useTranslation("client-scopes");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
{searchType === "type" && (
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<SearchDropdown
|
||||
searchType={searchType}
|
||||
onSelect={onSelect}
|
||||
withProtocol={!!protocol}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Select
|
||||
className="keycloak__client-scopes__searchtype"
|
||||
onToggle={(open) => setOpen(open)}
|
||||
isOpen={open}
|
||||
selections={[
|
||||
type === AllClientScopes.none
|
||||
? t("common:allTypes")
|
||||
: t(`common:clientScope.${type}`),
|
||||
]}
|
||||
onSelect={(_, value) => {
|
||||
onType(value as AllClientScopes);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<SelectOption value={AllClientScopes.none}>
|
||||
{t("common:allTypes")}
|
||||
</SelectOption>
|
||||
<>{clientScopeTypesSelectOptions(t)}</>
|
||||
</Select>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
)}
|
||||
{searchType === "protocol" && !!protocol && (
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<SearchDropdown
|
||||
searchType={searchType}
|
||||
onSelect={onSelect}
|
||||
withProtocol
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Select
|
||||
className="keycloak__client-scopes__searchtype"
|
||||
onToggle={(open) => setOpen(open)}
|
||||
isOpen={open}
|
||||
selections={[t(`protocolTypes.${protocol}`)]}
|
||||
onSelect={(_, value) => {
|
||||
onProtocol?.(value as ProtocolType);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
{PROTOCOLS.map((type) => (
|
||||
<SelectOption key={type} value={type}>
|
||||
{t(`protocolTypes.${type}`)}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -14,7 +14,7 @@ export default {
|
|||
deleteClientScope: "Delete client scope {{name}}",
|
||||
deleteClientScope_plural: "Delete {{count}} client scopes",
|
||||
deleteConfirm: "Are you sure you want to delete this client scope",
|
||||
changeType: "Change type",
|
||||
changeTypeTo: "Change type to",
|
||||
changeTypeIntro: "{{count}} selected client scopes will be changed to",
|
||||
clientScopeSuccess: "Scope mapping updated",
|
||||
clientScopeError: "Could not update scope mapping {{error}}",
|
||||
|
@ -62,5 +62,10 @@ export default {
|
|||
scope: "Scope",
|
||||
roleMappingUpdatedSuccess: "Role mapping updated",
|
||||
roleMappingUpdatedError: "Could not update role mapping {{error}}",
|
||||
protocolTypes: {
|
||||
all: "All",
|
||||
saml: "SAML",
|
||||
"openid-connect": "openid-connect",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -30,7 +30,6 @@ export default {
|
|||
searchByName: "Search by name",
|
||||
setup: "Setup",
|
||||
evaluate: "Evaluate",
|
||||
changeTypeTo: "Change type to",
|
||||
assignRole: "Assign role",
|
||||
unAssignRole: "Unassign",
|
||||
removeMappingTitle: "Remove mapping?",
|
||||
|
@ -38,8 +37,9 @@ export default {
|
|||
removeMappingConfirm_plural:
|
||||
"Are you sure you want to remove {{count}} mappings",
|
||||
clientScopeSearch: {
|
||||
client: "Client scope",
|
||||
assigned: "Assigned type",
|
||||
name: "Name",
|
||||
type: "Assigned type",
|
||||
protocol: "Protocol",
|
||||
},
|
||||
assignedClientScope: "Assigned client scope",
|
||||
assignedType: "Assigned type",
|
||||
|
|
|
@ -5,92 +5,57 @@ import {
|
|||
Button,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownToggle,
|
||||
KebabToggle,
|
||||
Select,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
import { FilterIcon } from "@patternfly/react-icons";
|
||||
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
|
||||
import type KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
||||
|
||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||
import { toUpperCase } from "../../util";
|
||||
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||
import { AddScopeDialog } from "./AddScopeDialog";
|
||||
import {
|
||||
clientScopeTypesSelectOptions,
|
||||
ClientScopeType,
|
||||
ClientScope,
|
||||
CellDropdown,
|
||||
AllClientScopes,
|
||||
AllClientScopeType,
|
||||
changeClientScope,
|
||||
addClientScope,
|
||||
removeClientScope,
|
||||
} from "../../components/client-scope/ClientScopeTypes";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||
import {
|
||||
nameFilter,
|
||||
SearchDropdown,
|
||||
SearchToolbar,
|
||||
SearchType,
|
||||
typeFilter,
|
||||
} from "../../client-scopes/details/SearchFilter";
|
||||
|
||||
import "./client-scopes.css";
|
||||
import { ChangeTypeDropdown } from "../../client-scopes/ChangeTypeDropdown";
|
||||
|
||||
export type ClientScopesProps = {
|
||||
clientId: string;
|
||||
protocol: string;
|
||||
};
|
||||
|
||||
type Row = ClientScopeRepresentation & {
|
||||
type: ClientScopeType;
|
||||
description: string;
|
||||
export type Row = ClientScopeRepresentation & {
|
||||
type: AllClientScopeType;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
const castAdminClient = (adminClient: KeycloakAdminClient) =>
|
||||
adminClient.clients as unknown as {
|
||||
[index: string]: Function;
|
||||
};
|
||||
|
||||
const changeScope = async (
|
||||
adminClient: KeycloakAdminClient,
|
||||
clientId: string,
|
||||
clientScope: ClientScopeRepresentation,
|
||||
type: ClientScopeType,
|
||||
changeTo: ClientScopeType
|
||||
) => {
|
||||
await removeScope(adminClient, clientId, clientScope, type);
|
||||
await addScope(adminClient, clientId, clientScope, changeTo);
|
||||
};
|
||||
|
||||
const removeScope = async (
|
||||
adminClient: KeycloakAdminClient,
|
||||
clientId: string,
|
||||
clientScope: ClientScopeRepresentation,
|
||||
type: ClientScopeType
|
||||
) => {
|
||||
const typeToName = toUpperCase(type);
|
||||
await castAdminClient(adminClient)[`del${typeToName}ClientScope`]({
|
||||
id: clientId,
|
||||
clientScopeId: clientScope.id!,
|
||||
});
|
||||
};
|
||||
|
||||
const addScope = async (
|
||||
adminClient: KeycloakAdminClient,
|
||||
clientId: string,
|
||||
clientScope: ClientScopeRepresentation,
|
||||
type: ClientScopeType
|
||||
) => {
|
||||
const typeToName = toUpperCase(type);
|
||||
await castAdminClient(adminClient)[`add${typeToName}ClientScope`]({
|
||||
id: clientId,
|
||||
clientScopeId: clientScope.id!,
|
||||
});
|
||||
};
|
||||
|
||||
type SearchType = "client" | "assigned";
|
||||
|
||||
export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const [searchToggle, setSearchToggle] = useState(false);
|
||||
const [searchType, setSearchType] = useState<SearchType>("client");
|
||||
const [addToggle, setAddToggle] = useState(false);
|
||||
const [searchType, setSearchType] = useState<SearchType>("name");
|
||||
|
||||
const [searchTypeType, setSearchTypeType] = useState<AllClientScopes>(
|
||||
AllClientScopes.none
|
||||
);
|
||||
|
||||
const [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||
const [kebabOpen, setKebabOpen] = useState(false);
|
||||
|
||||
|
@ -100,7 +65,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
|||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const loader = async () => {
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
const defaultClientScopes =
|
||||
await adminClient.clients.listDefaultClientScopes({ id: clientId });
|
||||
const optionalClientScopes =
|
||||
|
@ -112,20 +77,22 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
|||
|
||||
const optional = optionalClientScopes.map((c) => {
|
||||
const scope = find(c.id!);
|
||||
return {
|
||||
const row: Row = {
|
||||
...c,
|
||||
type: ClientScope.optional,
|
||||
description: scope.description,
|
||||
} as Row;
|
||||
};
|
||||
return row;
|
||||
});
|
||||
|
||||
const defaultScopes = defaultClientScopes.map((c) => {
|
||||
const scope = find(c.id!);
|
||||
return {
|
||||
const row: Row = {
|
||||
...c,
|
||||
type: ClientScope.default,
|
||||
description: scope.description,
|
||||
} as Row;
|
||||
};
|
||||
return row;
|
||||
});
|
||||
|
||||
const rows = [...optional, ...defaultScopes];
|
||||
|
@ -136,7 +103,9 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
|||
.filter((scope) => scope.protocol === protocol)
|
||||
);
|
||||
|
||||
return rows;
|
||||
const filter =
|
||||
searchType === "name" ? nameFilter(search) : typeFilter(searchTypeType);
|
||||
return rows.filter(filter).slice(first, max);
|
||||
};
|
||||
|
||||
const TypeSelector = (scope: Row) => (
|
||||
|
@ -145,7 +114,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
|||
type={scope.type}
|
||||
onSelect={async (value) => {
|
||||
try {
|
||||
await changeScope(
|
||||
await changeClientScope(
|
||||
adminClient,
|
||||
clientId,
|
||||
scope,
|
||||
|
@ -173,7 +142,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
|||
await Promise.all(
|
||||
scopes.map(
|
||||
async (scope) =>
|
||||
await addScope(
|
||||
await addClientScope(
|
||||
adminClient,
|
||||
clientId,
|
||||
scope.scope,
|
||||
|
@ -194,83 +163,41 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
|||
key={key}
|
||||
loader={loader}
|
||||
ariaLabelKey="clients:clientScopeList"
|
||||
searchPlaceholderKey="clients:searchByName"
|
||||
searchPlaceholderKey={
|
||||
searchType === "name" ? "clients:searchByName" : undefined
|
||||
}
|
||||
canSelectAll
|
||||
isPaginated
|
||||
isSearching={searchType === "type"}
|
||||
onSelect={(rows) => setSelectedRows([...rows])}
|
||||
searchTypeComponent={
|
||||
<Dropdown
|
||||
className="keycloak__client-scopes__searchtype"
|
||||
toggle={
|
||||
<DropdownToggle
|
||||
id="toggle-id"
|
||||
onToggle={() => setSearchToggle(!searchToggle)}
|
||||
>
|
||||
<FilterIcon /> {t(`clientScopeSearch.${searchType}`)}
|
||||
</DropdownToggle>
|
||||
}
|
||||
aria-label="Select Input"
|
||||
isOpen={searchToggle}
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key="client"
|
||||
onClick={() => {
|
||||
setSearchType("client");
|
||||
setSearchToggle(false);
|
||||
}}
|
||||
>
|
||||
{t("clientScopeSearch.client")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
key="assigned"
|
||||
onClick={() => {
|
||||
setSearchType("assigned");
|
||||
setSearchToggle(false);
|
||||
}}
|
||||
>
|
||||
{t("clientScopeSearch.assigned")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
<SearchDropdown
|
||||
searchType={searchType}
|
||||
onSelect={(searchType) => setSearchType(searchType)}
|
||||
/>
|
||||
}
|
||||
toolbarItem={
|
||||
<>
|
||||
<SearchToolbar
|
||||
searchType={searchType}
|
||||
type={searchTypeType}
|
||||
onSelect={(searchType) => setSearchType(searchType)}
|
||||
onType={(value) => {
|
||||
setSearchTypeType(value);
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
<ToolbarItem>
|
||||
<Button onClick={() => setAddDialogOpen(true)}>
|
||||
{t("addClientScope")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Select
|
||||
id="add-dropdown"
|
||||
key="add-dropdown"
|
||||
isOpen={addToggle}
|
||||
selections={[]}
|
||||
isDisabled={selectedRows.length === 0}
|
||||
placeholderText={t("changeTypeTo")}
|
||||
onToggle={() => setAddToggle(!addToggle)}
|
||||
onSelect={async (_, value) => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map((row) => {
|
||||
return changeScope(
|
||||
adminClient,
|
||||
clientId,
|
||||
{ ...row },
|
||||
row.type,
|
||||
value as ClientScope
|
||||
);
|
||||
})
|
||||
);
|
||||
setAddToggle(false);
|
||||
refresh();
|
||||
addAlert(t("clientScopeSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError("clients:clientScopeError", error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{clientScopeTypesSelectOptions(t)}
|
||||
</Select>
|
||||
<ChangeTypeDropdown
|
||||
clientId={clientId}
|
||||
selectedRows={selectedRows}
|
||||
refresh={refresh}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Dropdown
|
||||
|
@ -287,11 +214,11 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
|||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map(async (row) => {
|
||||
await removeScope(
|
||||
await removeClientScope(
|
||||
adminClient,
|
||||
clientId,
|
||||
{ ...row },
|
||||
row.type
|
||||
row.type as ClientScope
|
||||
);
|
||||
})
|
||||
);
|
||||
|
@ -331,7 +258,12 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
|
|||
title: t("common:remove"),
|
||||
onRowClick: async (row) => {
|
||||
try {
|
||||
await removeScope(adminClient, clientId, row, row.type);
|
||||
await removeClientScope(
|
||||
adminClient,
|
||||
clientId,
|
||||
row,
|
||||
row.type as ClientScope
|
||||
);
|
||||
addAlert(t("clientScopeRemoveSuccess"), AlertVariant.success);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
|
|
|
@ -65,6 +65,7 @@ export default {
|
|||
optional: "Optional",
|
||||
none: "None",
|
||||
},
|
||||
allTypes: "All types",
|
||||
|
||||
home: "Home",
|
||||
manage: "Manage",
|
||||
|
|
|
@ -6,6 +6,7 @@ import { DropdownItem, Select, SelectOption } from "@patternfly/react-core";
|
|||
|
||||
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
|
||||
import type KeycloakAdminClient from "@keycloak/keycloak-admin-client";
|
||||
import { toUpperCase } from "../../util";
|
||||
|
||||
export enum ClientScope {
|
||||
default = "default",
|
||||
|
@ -127,3 +128,44 @@ const addScope = async (
|
|||
id: clientScope.id!,
|
||||
});
|
||||
};
|
||||
|
||||
export const changeClientScope = async (
|
||||
adminClient: KeycloakAdminClient,
|
||||
clientId: string,
|
||||
clientScope: ClientScopeRepresentation,
|
||||
type: AllClientScopeType,
|
||||
changeTo: ClientScopeType
|
||||
) => {
|
||||
if (type !== "none") {
|
||||
await removeClientScope(adminClient, clientId, clientScope, type);
|
||||
}
|
||||
await addClientScope(adminClient, clientId, clientScope, changeTo);
|
||||
};
|
||||
|
||||
export const removeClientScope = async (
|
||||
adminClient: KeycloakAdminClient,
|
||||
clientId: string,
|
||||
clientScope: ClientScopeRepresentation,
|
||||
type: ClientScope
|
||||
) => {
|
||||
const methodName = `del${toUpperCase(type)}ClientScope` as const;
|
||||
|
||||
await adminClient.clients[methodName]({
|
||||
id: clientId,
|
||||
clientScopeId: clientScope.id!,
|
||||
});
|
||||
};
|
||||
|
||||
export const addClientScope = async (
|
||||
adminClient: KeycloakAdminClient,
|
||||
clientId: string,
|
||||
clientScope: ClientScopeRepresentation,
|
||||
type: ClientScopeType
|
||||
) => {
|
||||
const methodName = `add${toUpperCase(type)}ClientScope` as const;
|
||||
|
||||
await adminClient.clients[methodName]({
|
||||
id: clientId,
|
||||
clientScopeId: clientScope.id!,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -413,9 +413,7 @@ export function KeycloakDataTable<T>({
|
|||
)}
|
||||
{!loading &&
|
||||
(!data || data.length === 0) &&
|
||||
search !== "" &&
|
||||
!isSearching &&
|
||||
searchPlaceholderKey && (
|
||||
(search !== "" || isSearching) && (
|
||||
<ListEmptyState
|
||||
hasIcon={true}
|
||||
icon={icon}
|
||||
|
@ -427,7 +425,11 @@ export function KeycloakDataTable<T>({
|
|||
{loading && <Loading />}
|
||||
</PaginatingTableToolbar>
|
||||
)}
|
||||
{!loading && (!data || data?.length === 0) && search === "" && emptyState}
|
||||
{!loading &&
|
||||
(!data || data?.length === 0) &&
|
||||
search === "" &&
|
||||
!isSearching &&
|
||||
emptyState}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -51,8 +51,8 @@ export const exportClient = (client: ClientRepresentation): void => {
|
|||
);
|
||||
};
|
||||
|
||||
export const toUpperCase = (name: string) =>
|
||||
name.charAt(0).toUpperCase() + name.slice(1);
|
||||
export const toUpperCase = <T extends string>(name: T) =>
|
||||
(name.charAt(0).toUpperCase() + name.slice(1)) as Capitalize<T>;
|
||||
|
||||
export const convertToFormValues = (
|
||||
obj: any,
|
||||
|
|
Loading…
Reference in a new issue