Sort enabled features (#2557)

This commit is contained in:
Erik Jan de Wit 2022-05-09 12:44:07 +02:00 committed by GitHub
parent 58112be510
commit 93088c5380
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 200 additions and 143 deletions

View file

@ -65,11 +65,11 @@ describe("Authentication test", () => {
listingPage.goToItemDetails("Copy of browser"); listingPage.goToItemDetails("Copy of browser");
detailPage.addExecution( detailPage.addExecution(
"Copy of browser forms", "Copy of browser forms",
"console-username-password" "reset-credentials-choose-user"
); );
masthead.checkNotificationMessage("Flow successfully updated"); masthead.checkNotificationMessage("Flow successfully updated");
detailPage.executionExists("Username Password Challenge"); detailPage.executionExists("Choose User");
}); });
it("should add a condition", () => { it("should add a condition", () => {
@ -80,7 +80,6 @@ describe("Authentication test", () => {
); );
masthead.checkNotificationMessage("Flow successfully updated"); masthead.checkNotificationMessage("Flow successfully updated");
detailPage.executionExists("Username Password Challenge");
}); });
it("should add a sub-flow", () => { it("should add a sub-flow", () => {

View file

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
Button, Button,
@ -9,10 +9,11 @@ import {
PageSection, PageSection,
Radio, Radio,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation"; import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation";
import { PaginatingTableToolbar } from "../../../components/table-toolbar/PaginatingTableToolbar"; import { PaginatingTableToolbar } from "../../../components/table-toolbar/PaginatingTableToolbar";
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
import useLocaleSort, { mapByKey } from "../../../utils/useLocaleSort";
import { providerConditionFilter } from "../../FlowDetails"; import { providerConditionFilter } from "../../FlowDetails";
type AuthenticationProviderListProps = { type AuthenticationProviderListProps = {
@ -64,6 +65,7 @@ export const AddStepModal = ({ name, type, onSelect }: AddStepModalProps) => {
useState<AuthenticationProviderRepresentation[]>(); useState<AuthenticationProviderRepresentation[]>();
const [max, setMax] = useState(10); const [max, setMax] = useState(10);
const [first, setFirst] = useState(0); const [first, setFirst] = useState(0);
const localeSort = useLocaleSort();
useFetch( useFetch(
async () => { async () => {
@ -89,7 +91,14 @@ export const AddStepModal = ({ name, type, onSelect }: AddStepModalProps) => {
[] []
); );
const page = providers?.slice(first, first + max + 1); const page = useMemo(
() =>
localeSort(providers ?? [], mapByKey("displayName")).slice(
first,
first + max + 1
),
[providers, first, max]
);
return ( return (
<Modal <Modal
@ -121,7 +130,7 @@ export const AddStepModal = ({ name, type, onSelect }: AddStepModalProps) => {
> >
{providers && providers.length > max && ( {providers && providers.length > max && (
<PaginatingTableToolbar <PaginatingTableToolbar
count={page?.length || 0} count={page.length || 0}
first={first} first={first}
max={max} max={max}
onNextClick={setFirst} onNextClick={setFirst}

View file

@ -20,6 +20,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { emptyFormatter } from "../util"; import { emptyFormatter } from "../util";
import useLocaleSort, { mapByKey } from "../utils/useLocaleSort";
import { import {
CellDropdown, CellDropdown,
ClientScope, ClientScope,
@ -32,9 +33,7 @@ import {
import { ChangeTypeDropdown } from "./ChangeTypeDropdown"; import { ChangeTypeDropdown } from "./ChangeTypeDropdown";
import { toNewClientScope } from "./routes/NewClientScope"; import { toNewClientScope } from "./routes/NewClientScope";
import "./client-scope.css";
import { toClientScope } from "./routes/ClientScope"; import { toClientScope } from "./routes/ClientScope";
import { useWhoAmI } from "../context/whoami/WhoAmI";
import { import {
nameFilter, nameFilter,
protocolFilter, protocolFilter,
@ -48,9 +47,10 @@ import type { Row } from "../clients/scopes/ClientScopes";
import { getProtocolName } from "../clients/utils"; import { getProtocolName } from "../clients/utils";
import helpUrls from "../help-urls"; import helpUrls from "../help-urls";
import "./client-scope.css";
export default function ClientScopesSection() { export default function ClientScopesSection() {
const { realm } = useRealm(); const { realm } = useRealm();
const { whoAmI } = useWhoAmI();
const { t } = useTranslation("client-scopes"); const { t } = useTranslation("client-scopes");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
@ -66,6 +66,7 @@ export default function ClientScopesSection() {
AllClientScopes.none AllClientScopes.none
); );
const [searchProtocol, setSearchProtocol] = useState<ProtocolType>("all"); const [searchProtocol, setSearchProtocol] = useState<ProtocolType>("all");
const localeSort = useLocaleSort();
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const refresh = () => { const refresh = () => {
@ -87,7 +88,7 @@ export default function ClientScopesSection() {
? typeFilter(searchTypeType) ? typeFilter(searchTypeType)
: protocolFilter(searchProtocol); : protocolFilter(searchProtocol);
return clientScopes const transformed = clientScopes
.map((scope) => { .map((scope) => {
const row: Row = { const row: Row = {
...scope, ...scope,
@ -103,9 +104,12 @@ export default function ClientScopesSection() {
}; };
return row; return row;
}) })
.filter(filter) .filter(filter);
.sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale()))
.slice(first, Number(first) + Number(max)); return localeSort(transformed, mapByKey("name")).slice(
first,
Number(first) + Number(max)
);
}; };
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({

View file

@ -19,9 +19,9 @@ import type ProtocolMapperRepresentation from "@keycloak/keycloak-admin-client/l
import type { ProtocolMapperTypeRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/serverInfoRepesentation"; import type { ProtocolMapperTypeRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/serverInfoRepesentation";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { useWhoAmI } from "../../context/whoami/WhoAmI";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort";
type Row = { type Row = {
name: string; name: string;
@ -46,28 +46,26 @@ export const AddMapperDialog = (props: AddMapperDialogProps) => {
const { t } = useTranslation("client-scopes"); const { t } = useTranslation("client-scopes");
const serverInfo = useServerInfo(); const serverInfo = useServerInfo();
const { whoAmI } = useWhoAmI();
const protocol = props.protocol; const protocol = props.protocol;
const protocolMappers = serverInfo.protocolMapperTypes![protocol]; const protocolMappers = serverInfo.protocolMapperTypes![protocol];
const builtInMappers = serverInfo.builtinProtocolMappers![protocol]; const builtInMappers = serverInfo.builtinProtocolMappers![protocol];
const [filter, setFilter] = useState<ProtocolMapperRepresentation[]>([]); const [filter, setFilter] = useState<ProtocolMapperRepresentation[]>([]);
const [selectedRows, setSelectedRows] = useState<Row[]>([]); const [selectedRows, setSelectedRows] = useState<Row[]>([]);
const localeSort = useLocaleSort();
const allRows = useMemo( const allRows = useMemo(
() => () =>
builtInMappers localeSort(builtInMappers, mapByKey("name")).map((mapper) => {
.sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale())) const mapperType = protocolMappers.filter(
.map((mapper) => { (type) => type.id === mapper.protocolMapper
const mapperType = protocolMappers.filter( )[0];
(type) => type.id === mapper.protocolMapper return {
)[0]; item: mapper,
return { name: mapper.name!,
item: mapper, description: mapperType.helpText,
name: mapper.name!, };
description: mapperType.helpText, }),
}; [builtInMappers, protocolMappers]
}),
[]
); );
const [rows, setRows] = useState(allRows); const [rows, setRows] = useState(allRows);
@ -78,10 +76,7 @@ export const AddMapperDialog = (props: AddMapperDialogProps) => {
} }
const sortedProtocolMappers = useMemo( const sortedProtocolMappers = useMemo(
() => () => localeSort(protocolMappers, mapByKey("name")),
protocolMappers.sort((a, b) =>
a.name!.localeCompare(b.name!, whoAmI.getLocale())
),
[protocolMappers] [protocolMappers]
); );

View file

@ -16,7 +16,7 @@ import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
import { HelpItem } from "../../../components/help-enabler/HelpItem"; import { HelpItem } from "../../../components/help-enabler/HelpItem";
import { AddScopeDialog } from "../../scopes/AddScopeDialog"; import { AddScopeDialog } from "../../scopes/AddScopeDialog";
import { useWhoAmI } from "../../../context/whoami/WhoAmI"; import useLocaleSort, { mapByKey } from "../../../utils/useLocaleSort";
export type RequiredIdValue = { export type RequiredIdValue = {
id: string; id: string;
@ -41,7 +41,7 @@ export const ClientScope = () => {
>([]); >([]);
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { whoAmI } = useWhoAmI(); const localeSort = useLocaleSort();
useFetch( useFetch(
() => adminClient.clientScopes.find(), () => adminClient.clientScopes.find(),
@ -49,11 +49,7 @@ export const ClientScope = () => {
setSelectedScopes( setSelectedScopes(
getValues("clientScopes").map((s) => scopes.find((c) => c.id === s.id)!) getValues("clientScopes").map((s) => scopes.find((c) => c.id === s.id)!)
); );
setScopes( setScopes(localeSort(scopes, mapByKey("name")));
scopes.sort((a, b) =>
a.name!.localeCompare(b.name!, whoAmI.getLocale())
)
);
}, },
[] []
); );

View file

@ -36,7 +36,7 @@ import { ChangeTypeDropdown } from "../../client-scopes/ChangeTypeDropdown";
import { toDedicatedScope } from "../routes/DedicatedScopeDetails"; import { toDedicatedScope } from "../routes/DedicatedScopeDetails";
import { useRealm } from "../../context/realm-context/RealmContext"; import { useRealm } from "../../context/realm-context/RealmContext";
import { useWhoAmI } from "../../context/whoami/WhoAmI"; import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort";
import "./client-scopes.css"; import "./client-scopes.css";
@ -60,9 +60,9 @@ export const ClientScopes = ({
}: ClientScopesProps) => { }: ClientScopesProps) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { whoAmI } = useWhoAmI();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const { realm } = useRealm(); const { realm } = useRealm();
const localeSort = useLocaleSort();
const [searchType, setSearchType] = useState<SearchType>("name"); const [searchType, setSearchType] = useState<SearchType>("name");
@ -116,13 +116,15 @@ export const ClientScopes = ({
clientScopes clientScopes
.filter((scope) => !names.includes(scope.name)) .filter((scope) => !names.includes(scope.name))
.filter((scope) => scope.protocol === protocol) .filter((scope) => scope.protocol === protocol)
.sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale()))
); );
const filter = const filter =
searchType === "name" ? nameFilter(search) : typeFilter(searchTypeType); searchType === "name" ? nameFilter(search) : typeFilter(searchTypeType);
const firstNum = Number(first); const firstNum = Number(first);
const page = rows.filter(filter).slice(firstNum, firstNum + Number(max)); const page = localeSort(rows.filter(filter), mapByKey("name")).slice(
firstNum,
firstNum + Number(max)
);
if (firstNum === 0) { if (firstNum === 0) {
return [ return [
{ {

View file

@ -11,7 +11,7 @@ import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation"; import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
import { useParams } from "react-router"; import { useParams } from "react-router";
import type { EditClientPolicyConditionParams } from "../../realm-settings/routes/EditCondition"; import type { EditClientPolicyConditionParams } from "../../realm-settings/routes/EditCondition";
import { useWhoAmI } from "../../context/whoami/WhoAmI"; import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort";
export const MultivaluedScopesComponent = ({ export const MultivaluedScopesComponent = ({
defaultValue, defaultValue,
@ -22,8 +22,8 @@ export const MultivaluedScopesComponent = ({
const { control } = useFormContext(); const { control } = useFormContext();
const { conditionName } = useParams<EditClientPolicyConditionParams>(); const { conditionName } = useParams<EditClientPolicyConditionParams>();
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { whoAmI } = useWhoAmI();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const localeSort = useLocaleSort();
const [clientScopes, setClientScopes] = useState<ClientScopeRepresentation[]>( const [clientScopes, setClientScopes] = useState<ClientScopeRepresentation[]>(
[] []
); );
@ -62,11 +62,12 @@ export const MultivaluedScopesComponent = ({
<> <>
{open && ( {open && (
<AddScopeDialog <AddScopeDialog
clientScopes={clientScopes clientScopes={localeSort(
.filter((scope) => !value.includes(scope.name!)) clientScopes.filter(
.sort((a, b) => (scope) => !value.includes(scope.name!)
a.name!.localeCompare(b.name!, whoAmI.getLocale()) ),
)} mapByKey("name")
)}
isClientScopesConditionType isClientScopesConditionType
open={open} open={open}
toggleDialog={() => setOpen(!open)} toggleDialog={() => setOpen(!open)}

View file

@ -26,7 +26,7 @@ import { useRealm } from "../../context/realm-context/RealmContext";
import { toPermissionDetails } from "../../clients/routes/PermissionDetails"; import { toPermissionDetails } from "../../clients/routes/PermissionDetails";
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner"; import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
import { HelpItem } from "../../components/help-enabler/HelpItem"; import { HelpItem } from "../../components/help-enabler/HelpItem";
import { useWhoAmI } from "../../context/whoami/WhoAmI"; import useLocaleSort from "../../utils/useLocaleSort";
import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog";
import "./permissions-tab.css"; import "./permissions-tab.css";
@ -43,9 +43,9 @@ export const PermissionsTab = ({ id, type }: PermissionsTabProps) => {
const history = useHistory(); const history = useHistory();
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { realm } = useRealm(); const { realm } = useRealm();
const { whoAmI } = useWhoAmI();
const [realmId, setRealmId] = useState(""); const [realmId, setRealmId] = useState("");
const [permission, setPermission] = useState<ManagementPermissionReference>(); const [permission, setPermission] = useState<ManagementPermissionReference>();
const localeSort = useLocaleSort();
const togglePermissionEnabled = (enabled: boolean) => { const togglePermissionEnabled = (enabled: boolean) => {
switch (type) { switch (type) {
@ -173,48 +173,47 @@ export const PermissionsTab = ({ id, type }: PermissionsTabProps) => {
</Tr> </Tr>
</Thead> </Thead>
<Tbody> <Tbody>
{Object.entries(permission.scopePermissions || {}) {localeSort(
.sort((a, b) => Object.entries(permission.scopePermissions || {}),
a[0]!.localeCompare(b[0]!, whoAmI.getLocale()) ([name]) => name
) ).map(([name, id]) => (
.map(([name, id]) => ( <Tr key={id}>
<Tr key={id}> <Td>
<Td> <Link
<Link to={toPermissionDetails({
to={toPermissionDetails({ realm,
realm, id: realmId,
id: realmId, permissionType: "scope",
permissionType: "scope", permissionId: id,
permissionId: id, })}
})} >
> {name}
{name} </Link>
</Link> </Td>
</Td> <Td>
<Td> {t(`scopePermissions.${type}.${name}-description`)}
{t(`scopePermissions.${type}.${name}-description`)} </Td>
</Td> <Td isActionCell>
<Td isActionCell> <ActionsColumn
<ActionsColumn items={[
items={[ {
{ title: t("common:edit"),
title: t("common:edit"), onClick() {
onClick() { history.push(
history.push( toPermissionDetails({
toPermissionDetails({ realm,
realm, id: realmId,
id: realmId, permissionType: "scope",
permissionType: "scope", permissionId: id,
permissionId: id, })
}) );
);
},
}, },
]} },
/> ]}
</Td> />
</Tr> </Td>
))} </Tr>
))}
</Tbody> </Tbody>
</TableComposable> </TableComposable>
</CardBody> </CardBody>

View file

@ -1,4 +1,4 @@
import React from "react"; import React, { useMemo } from "react";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { xor } from "lodash-es"; import { xor } from "lodash-es";
@ -33,14 +33,15 @@ import { toUpperCase } from "../util";
import { HelpItem } from "../components/help-enabler/HelpItem"; import { HelpItem } from "../components/help-enabler/HelpItem";
import environment from "../environment"; import environment from "../environment";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import useLocaleSort from "../utils/useLocaleSort";
import { import {
routableTab, routableTab,
RoutableTabs, RoutableTabs,
} from "../components/routable-tabs/RoutableTabs"; } from "../components/routable-tabs/RoutableTabs";
import { DashboardTab, toDashboard } from "./routes/Dashboard"; import { DashboardTab, toDashboard } from "./routes/Dashboard";
import { ProviderInfo } from "./ProviderInfo";
import "./dashboard.css"; import "./dashboard.css";
import { ProviderInfo } from "./ProviderInfo";
const EmptyDashboard = () => { const EmptyDashboard = () => {
const { t } = useTranslation("dashboard"); const { t } = useTranslation("dashboard");
@ -70,11 +71,28 @@ const Dashboard = () => {
const { realm } = useRealm(); const { realm } = useRealm();
const serverInfo = useServerInfo(); const serverInfo = useServerInfo();
const history = useHistory(); const history = useHistory();
const localeSort = useLocaleSort();
const enabledFeatures = xor( const enabledFeatures = useMemo(
serverInfo.profileInfo?.disabledFeatures, () =>
serverInfo.profileInfo?.experimentalFeatures, localeSort(
serverInfo.profileInfo?.previewFeatures xor(
serverInfo.profileInfo?.disabledFeatures,
serverInfo.profileInfo?.experimentalFeatures,
serverInfo.profileInfo?.previewFeatures
),
(item) => item
),
[serverInfo.profileInfo]
);
const disabledFeatures = useMemo(
() =>
localeSort(
serverInfo.profileInfo?.disabledFeatures ?? [],
(item) => item
),
[serverInfo.profileInfo]
); );
const isExperimentalFeature = (feature: string) => const isExperimentalFeature = (feature: string) =>
@ -193,11 +211,9 @@ const Dashboard = () => {
</DescriptionListTerm> </DescriptionListTerm>
<DescriptionListDescription> <DescriptionListDescription>
<List variant={ListVariant.inline}> <List variant={ListVariant.inline}>
{serverInfo.profileInfo?.disabledFeatures?.map( {disabledFeatures.map((feature) => (
(feature) => ( <ListItem key={feature}>{feature}</ListItem>
<ListItem key={feature}>{feature}</ListItem> ))}
)
)}
</List> </List>
</DescriptionListDescription> </DescriptionListDescription>
</DescriptionListGroup> </DescriptionListGroup>

View file

@ -12,46 +12,47 @@ import type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/de
import { KeycloakDataTable } from "../../../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../../../components/table-toolbar/KeycloakDataTable";
import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState";
import { useAlerts } from "../../../components/alert/Alerts"; import { useAlerts } from "../../../components/alert/Alerts";
import { useAdminClient } from "../../../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog";
import { useWhoAmI } from "../../../context/whoami/WhoAmI"; import useLocaleSort, { mapByKey } from "../../../utils/useLocaleSort";
export const LdapMapperList = () => { export const LdapMapperList = () => {
const history = useHistory(); const history = useHistory();
const { t } = useTranslation("user-federation"); const { t } = useTranslation("user-federation");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const { whoAmI } = useWhoAmI();
const { url } = useRouteMatch(); const { url } = useRouteMatch();
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const refresh = () => setKey(key + 1); const refresh = () => setKey(key + 1);
const [mappers, setMappers] = useState<ComponentRepresentation[]>([]);
const localeSort = useLocaleSort();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const [selectedMapper, setSelectedMapper] = const [selectedMapper, setSelectedMapper] =
useState<ComponentRepresentation>(); useState<ComponentRepresentation>();
const loader = async () => { useFetch(
const testParams: { () =>
[name: string]: string | number; adminClient.components.find({
} = { parent: id,
parent: id, type: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
type: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper", }),
}; (mapper) => {
setMappers(
const mappersList = (await adminClient.components.find(testParams)).map( localeSort(
(mapper) => { mapper.map((mapper) => ({
return { ...mapper,
...mapper, name: mapper.name,
name: mapper.name, type: mapper.providerId,
type: mapper.providerId, })),
} as ComponentRepresentation; mapByKey("name")
} )
); );
return mappersList.sort((a, b) => },
a.name!.localeCompare(b.name!, whoAmI.getLocale()) [key]
); );
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: t("common:deleteMappingTitle", { mapperId: selectedMapper?.id }), titleKey: t("common:deleteMappingTitle", { mapperId: selectedMapper?.id }),
@ -88,7 +89,7 @@ export const LdapMapperList = () => {
<DeleteConfirm /> <DeleteConfirm />
<KeycloakDataTable <KeycloakDataTable
key={key} key={key}
loader={loader} loader={mappers}
ariaLabelKey="ldapMappersList" ariaLabelKey="ldapMappersList"
searchPlaceholderKey="common:searchForMapper" searchPlaceholderKey="common:searchForMapper"
toolbarItem={ toolbarItem={

View file

@ -10,8 +10,8 @@ import {
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import type CredentialRepresentation from "@keycloak/keycloak-admin-client/lib/defs/credentialRepresentation"; import type CredentialRepresentation from "@keycloak/keycloak-admin-client/lib/defs/credentialRepresentation";
import { useWhoAmI } from "../../context/whoami/WhoAmI";
import useToggle from "../../utils/useToggle"; import useToggle from "../../utils/useToggle";
import useLocaleSort from "../../utils/useLocaleSort";
import { CredentialDataDialog } from "./CredentialDataDialog"; import { CredentialDataDialog } from "./CredentialDataDialog";
type CredentialRowProps = { type CredentialRowProps = {
@ -30,8 +30,7 @@ export const CredentialRow = ({
const { t } = useTranslation("users"); const { t } = useTranslation("users");
const [showData, toggleShow] = useToggle(); const [showData, toggleShow] = useToggle();
const [kebabOpen, toggleKebab] = useToggle(); const [kebabOpen, toggleKebab] = useToggle();
const localeSort = useLocaleSort();
const { whoAmI } = useWhoAmI();
const rows = useMemo(() => { const rows = useMemo(() => {
if (!credential.credentialData) { if (!credential.credentialData) {
@ -41,17 +40,15 @@ export const CredentialRow = ({
const credentialData: Record<string, unknown> = JSON.parse( const credentialData: Record<string, unknown> = JSON.parse(
credential.credentialData credential.credentialData
); );
const locale = whoAmI.getLocale(); return localeSort(Object.entries(credentialData), ([key]) => key).map<
[string, string]
>(([key, value]) => {
if (typeof value === "string") {
return [key, value];
}
return Object.entries(credentialData) return [key, JSON.stringify(value)];
.sort(([a], [b]) => a.localeCompare(b, locale)) });
.map<[string, string]>(([key, value]) => {
if (typeof value === "string") {
return [key, value];
}
return [key, JSON.stringify(value)];
});
}, [credential.credentialData]); }, [credential.credentialData]);
return ( return (

View file

@ -0,0 +1,38 @@
import { useWhoAmI } from "../context/whoami/WhoAmI";
export type ValueMapperFn<T> = (item: T) => string | undefined;
export default function useLocaleSort() {
const { whoAmI } = useWhoAmI();
return function localeSort<T>(items: T[], mapperFn: ValueMapperFn<T>): T[] {
const locale = whoAmI.getLocale();
return [...items].sort((a, b) => {
const valA = mapperFn(a);
const valB = mapperFn(b);
if (valA === undefined || valB === undefined) {
return 0;
}
return valA.localeCompare(valB, locale);
});
};
}
// TODO: This might be built into TypeScript into future.
// See: https://github.com/microsoft/TypeScript/issues/48992
type KeysMatching<T, V> = {
[K in keyof T]: T[K] extends V ? K : never;
}[keyof T];
export const mapByKey =
<
T extends { [_ in K]?: string },
K extends KeysMatching<T, string | undefined>
>(
key: K
) =>
(item: T) =>
item[key];