From 93088c53807712b429651f91aed8dd1e39794a03 Mon Sep 17 00:00:00 2001 From: Erik Jan de Wit Date: Mon, 9 May 2022 12:44:07 +0200 Subject: [PATCH] Sort enabled features (#2557) --- .../integration/authentication_test.spec.ts | 5 +- .../components/modals/AddStepModal.tsx | 17 +++- src/client-scopes/ClientScopesSection.tsx | 18 ++-- src/client-scopes/add/MapperDialog.tsx | 33 +++---- .../authorization/policy/ClientScope.tsx | 10 +-- src/clients/scopes/ClientScopes.tsx | 10 ++- .../dynamic/MultivaluedScopesComponent.tsx | 15 ++-- .../permission-tab/PermissionTab.tsx | 85 +++++++++---------- src/dashboard/Dashboard.tsx | 38 ++++++--- .../ldap/mappers/LdapMapperList.tsx | 51 +++++------ src/user/user-credentials/CredentialRow.tsx | 23 +++-- src/utils/useLocaleSort.ts | 38 +++++++++ 12 files changed, 200 insertions(+), 143 deletions(-) create mode 100644 src/utils/useLocaleSort.ts diff --git a/cypress/integration/authentication_test.spec.ts b/cypress/integration/authentication_test.spec.ts index cfc692ca90..28981e2dcb 100644 --- a/cypress/integration/authentication_test.spec.ts +++ b/cypress/integration/authentication_test.spec.ts @@ -65,11 +65,11 @@ describe("Authentication test", () => { listingPage.goToItemDetails("Copy of browser"); detailPage.addExecution( "Copy of browser forms", - "console-username-password" + "reset-credentials-choose-user" ); masthead.checkNotificationMessage("Flow successfully updated"); - detailPage.executionExists("Username Password Challenge"); + detailPage.executionExists("Choose User"); }); it("should add a condition", () => { @@ -80,7 +80,6 @@ describe("Authentication test", () => { ); masthead.checkNotificationMessage("Flow successfully updated"); - detailPage.executionExists("Username Password Challenge"); }); it("should add a sub-flow", () => { diff --git a/src/authentication/components/modals/AddStepModal.tsx b/src/authentication/components/modals/AddStepModal.tsx index 91083832f9..71c11ca410 100644 --- a/src/authentication/components/modals/AddStepModal.tsx +++ b/src/authentication/components/modals/AddStepModal.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Button, @@ -9,10 +9,11 @@ import { PageSection, Radio, } from "@patternfly/react-core"; - import type { AuthenticationProviderRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/authenticatorConfigRepresentation"; + import { PaginatingTableToolbar } from "../../../components/table-toolbar/PaginatingTableToolbar"; import { useAdminClient, useFetch } from "../../../context/auth/AdminClient"; +import useLocaleSort, { mapByKey } from "../../../utils/useLocaleSort"; import { providerConditionFilter } from "../../FlowDetails"; type AuthenticationProviderListProps = { @@ -64,6 +65,7 @@ export const AddStepModal = ({ name, type, onSelect }: AddStepModalProps) => { useState(); const [max, setMax] = useState(10); const [first, setFirst] = useState(0); + const localeSort = useLocaleSort(); useFetch( 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 ( { > {providers && providers.length > max && ( ("all"); + const localeSort = useLocaleSort(); const [key, setKey] = useState(0); const refresh = () => { @@ -87,7 +88,7 @@ export default function ClientScopesSection() { ? typeFilter(searchTypeType) : protocolFilter(searchProtocol); - return clientScopes + const transformed = clientScopes .map((scope) => { const row: Row = { ...scope, @@ -103,9 +104,12 @@ export default function ClientScopesSection() { }; return row; }) - .filter(filter) - .sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale())) - .slice(first, Number(first) + Number(max)); + .filter(filter); + + return localeSort(transformed, mapByKey("name")).slice( + first, + Number(first) + Number(max) + ); }; const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ diff --git a/src/client-scopes/add/MapperDialog.tsx b/src/client-scopes/add/MapperDialog.tsx index 96e867c14c..4bf6ce21fe 100644 --- a/src/client-scopes/add/MapperDialog.tsx +++ b/src/client-scopes/add/MapperDialog.tsx @@ -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 { useServerInfo } from "../../context/server-info/ServerInfoProvider"; -import { useWhoAmI } from "../../context/whoami/WhoAmI"; import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState"; import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable"; +import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort"; type Row = { name: string; @@ -46,28 +46,26 @@ export const AddMapperDialog = (props: AddMapperDialogProps) => { const { t } = useTranslation("client-scopes"); const serverInfo = useServerInfo(); - const { whoAmI } = useWhoAmI(); const protocol = props.protocol; const protocolMappers = serverInfo.protocolMapperTypes![protocol]; const builtInMappers = serverInfo.builtinProtocolMappers![protocol]; const [filter, setFilter] = useState([]); const [selectedRows, setSelectedRows] = useState([]); + const localeSort = useLocaleSort(); const allRows = useMemo( () => - builtInMappers - .sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale())) - .map((mapper) => { - const mapperType = protocolMappers.filter( - (type) => type.id === mapper.protocolMapper - )[0]; - return { - item: mapper, - name: mapper.name!, - description: mapperType.helpText, - }; - }), - [] + localeSort(builtInMappers, mapByKey("name")).map((mapper) => { + const mapperType = protocolMappers.filter( + (type) => type.id === mapper.protocolMapper + )[0]; + return { + item: mapper, + name: mapper.name!, + description: mapperType.helpText, + }; + }), + [builtInMappers, protocolMappers] ); const [rows, setRows] = useState(allRows); @@ -78,10 +76,7 @@ export const AddMapperDialog = (props: AddMapperDialogProps) => { } const sortedProtocolMappers = useMemo( - () => - protocolMappers.sort((a, b) => - a.name!.localeCompare(b.name!, whoAmI.getLocale()) - ), + () => localeSort(protocolMappers, mapByKey("name")), [protocolMappers] ); diff --git a/src/clients/authorization/policy/ClientScope.tsx b/src/clients/authorization/policy/ClientScope.tsx index 4530cc64d4..3924f89d49 100644 --- a/src/clients/authorization/policy/ClientScope.tsx +++ b/src/clients/authorization/policy/ClientScope.tsx @@ -16,7 +16,7 @@ import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/ import { useAdminClient, useFetch } from "../../../context/auth/AdminClient"; import { HelpItem } from "../../../components/help-enabler/HelpItem"; import { AddScopeDialog } from "../../scopes/AddScopeDialog"; -import { useWhoAmI } from "../../../context/whoami/WhoAmI"; +import useLocaleSort, { mapByKey } from "../../../utils/useLocaleSort"; export type RequiredIdValue = { id: string; @@ -41,7 +41,7 @@ export const ClientScope = () => { >([]); const adminClient = useAdminClient(); - const { whoAmI } = useWhoAmI(); + const localeSort = useLocaleSort(); useFetch( () => adminClient.clientScopes.find(), @@ -49,11 +49,7 @@ export const ClientScope = () => { setSelectedScopes( getValues("clientScopes").map((s) => scopes.find((c) => c.id === s.id)!) ); - setScopes( - scopes.sort((a, b) => - a.name!.localeCompare(b.name!, whoAmI.getLocale()) - ) - ); + setScopes(localeSort(scopes, mapByKey("name"))); }, [] ); diff --git a/src/clients/scopes/ClientScopes.tsx b/src/clients/scopes/ClientScopes.tsx index a782749083..80bd7cfe5b 100644 --- a/src/clients/scopes/ClientScopes.tsx +++ b/src/clients/scopes/ClientScopes.tsx @@ -36,7 +36,7 @@ import { ChangeTypeDropdown } from "../../client-scopes/ChangeTypeDropdown"; import { toDedicatedScope } from "../routes/DedicatedScopeDetails"; import { useRealm } from "../../context/realm-context/RealmContext"; -import { useWhoAmI } from "../../context/whoami/WhoAmI"; +import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort"; import "./client-scopes.css"; @@ -60,9 +60,9 @@ export const ClientScopes = ({ }: ClientScopesProps) => { const { t } = useTranslation("clients"); const adminClient = useAdminClient(); - const { whoAmI } = useWhoAmI(); const { addAlert, addError } = useAlerts(); const { realm } = useRealm(); + const localeSort = useLocaleSort(); const [searchType, setSearchType] = useState("name"); @@ -116,13 +116,15 @@ export const ClientScopes = ({ clientScopes .filter((scope) => !names.includes(scope.name)) .filter((scope) => scope.protocol === protocol) - .sort((a, b) => a.name!.localeCompare(b.name!, whoAmI.getLocale())) ); const filter = searchType === "name" ? nameFilter(search) : typeFilter(searchTypeType); 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) { return [ { diff --git a/src/components/dynamic/MultivaluedScopesComponent.tsx b/src/components/dynamic/MultivaluedScopesComponent.tsx index 452a6eb9a4..9b86da4138 100644 --- a/src/components/dynamic/MultivaluedScopesComponent.tsx +++ b/src/components/dynamic/MultivaluedScopesComponent.tsx @@ -11,7 +11,7 @@ import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation"; import { useParams } from "react-router"; import type { EditClientPolicyConditionParams } from "../../realm-settings/routes/EditCondition"; -import { useWhoAmI } from "../../context/whoami/WhoAmI"; +import useLocaleSort, { mapByKey } from "../../utils/useLocaleSort"; export const MultivaluedScopesComponent = ({ defaultValue, @@ -22,8 +22,8 @@ export const MultivaluedScopesComponent = ({ const { control } = useFormContext(); const { conditionName } = useParams(); const adminClient = useAdminClient(); - const { whoAmI } = useWhoAmI(); const [open, setOpen] = useState(false); + const localeSort = useLocaleSort(); const [clientScopes, setClientScopes] = useState( [] ); @@ -62,11 +62,12 @@ export const MultivaluedScopesComponent = ({ <> {open && ( !value.includes(scope.name!)) - .sort((a, b) => - a.name!.localeCompare(b.name!, whoAmI.getLocale()) - )} + clientScopes={localeSort( + clientScopes.filter( + (scope) => !value.includes(scope.name!) + ), + mapByKey("name") + )} isClientScopesConditionType open={open} toggleDialog={() => setOpen(!open)} diff --git a/src/components/permission-tab/PermissionTab.tsx b/src/components/permission-tab/PermissionTab.tsx index 1ed3d84ddd..3db1580a81 100644 --- a/src/components/permission-tab/PermissionTab.tsx +++ b/src/components/permission-tab/PermissionTab.tsx @@ -26,7 +26,7 @@ import { useRealm } from "../../context/realm-context/RealmContext"; import { toPermissionDetails } from "../../clients/routes/PermissionDetails"; import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner"; 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 "./permissions-tab.css"; @@ -43,9 +43,9 @@ export const PermissionsTab = ({ id, type }: PermissionsTabProps) => { const history = useHistory(); const adminClient = useAdminClient(); const { realm } = useRealm(); - const { whoAmI } = useWhoAmI(); const [realmId, setRealmId] = useState(""); const [permission, setPermission] = useState(); + const localeSort = useLocaleSort(); const togglePermissionEnabled = (enabled: boolean) => { switch (type) { @@ -173,48 +173,47 @@ export const PermissionsTab = ({ id, type }: PermissionsTabProps) => { - {Object.entries(permission.scopePermissions || {}) - .sort((a, b) => - a[0]!.localeCompare(b[0]!, whoAmI.getLocale()) - ) - .map(([name, id]) => ( - - - - {name} - - - - {t(`scopePermissions.${type}.${name}-description`)} - - - name + ).map(([name, id]) => ( + + + + {name} + + + + {t(`scopePermissions.${type}.${name}-description`)} + + + - - - ))} + }, + ]} + /> + + + ))} diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx index 8d3cd4aa59..e01f212ada 100644 --- a/src/dashboard/Dashboard.tsx +++ b/src/dashboard/Dashboard.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { useHistory } from "react-router-dom"; import { Trans, useTranslation } from "react-i18next"; import { xor } from "lodash-es"; @@ -33,14 +33,15 @@ import { toUpperCase } from "../util"; import { HelpItem } from "../components/help-enabler/HelpItem"; import environment from "../environment"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; +import useLocaleSort from "../utils/useLocaleSort"; import { routableTab, RoutableTabs, } from "../components/routable-tabs/RoutableTabs"; import { DashboardTab, toDashboard } from "./routes/Dashboard"; +import { ProviderInfo } from "./ProviderInfo"; import "./dashboard.css"; -import { ProviderInfo } from "./ProviderInfo"; const EmptyDashboard = () => { const { t } = useTranslation("dashboard"); @@ -70,11 +71,28 @@ const Dashboard = () => { const { realm } = useRealm(); const serverInfo = useServerInfo(); const history = useHistory(); + const localeSort = useLocaleSort(); - const enabledFeatures = xor( - serverInfo.profileInfo?.disabledFeatures, - serverInfo.profileInfo?.experimentalFeatures, - serverInfo.profileInfo?.previewFeatures + const enabledFeatures = useMemo( + () => + localeSort( + 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) => @@ -193,11 +211,9 @@ const Dashboard = () => { - {serverInfo.profileInfo?.disabledFeatures?.map( - (feature) => ( - {feature} - ) - )} + {disabledFeatures.map((feature) => ( + {feature} + ))} diff --git a/src/user-federation/ldap/mappers/LdapMapperList.tsx b/src/user-federation/ldap/mappers/LdapMapperList.tsx index 6c74b6af4a..fb8b2f88e7 100644 --- a/src/user-federation/ldap/mappers/LdapMapperList.tsx +++ b/src/user-federation/ldap/mappers/LdapMapperList.tsx @@ -12,46 +12,47 @@ import type ComponentRepresentation from "@keycloak/keycloak-admin-client/lib/de import { KeycloakDataTable } from "../../../components/table-toolbar/KeycloakDataTable"; import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState"; 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 { useWhoAmI } from "../../../context/whoami/WhoAmI"; +import useLocaleSort, { mapByKey } from "../../../utils/useLocaleSort"; export const LdapMapperList = () => { const history = useHistory(); const { t } = useTranslation("user-federation"); const adminClient = useAdminClient(); const { addAlert, addError } = useAlerts(); - const { whoAmI } = useWhoAmI(); const { url } = useRouteMatch(); const [key, setKey] = useState(0); const refresh = () => setKey(key + 1); + const [mappers, setMappers] = useState([]); + const localeSort = useLocaleSort(); + const { id } = useParams<{ id: string }>(); const [selectedMapper, setSelectedMapper] = useState(); - const loader = async () => { - const testParams: { - [name: string]: string | number; - } = { - parent: id, - type: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper", - }; - - const mappersList = (await adminClient.components.find(testParams)).map( - (mapper) => { - return { - ...mapper, - name: mapper.name, - type: mapper.providerId, - } as ComponentRepresentation; - } - ); - return mappersList.sort((a, b) => - a.name!.localeCompare(b.name!, whoAmI.getLocale()) - ); - }; + useFetch( + () => + adminClient.components.find({ + parent: id, + type: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper", + }), + (mapper) => { + setMappers( + localeSort( + mapper.map((mapper) => ({ + ...mapper, + name: mapper.name, + type: mapper.providerId, + })), + mapByKey("name") + ) + ); + }, + [key] + ); const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ titleKey: t("common:deleteMappingTitle", { mapperId: selectedMapper?.id }), @@ -88,7 +89,7 @@ export const LdapMapperList = () => { { if (!credential.credentialData) { @@ -41,17 +40,15 @@ export const CredentialRow = ({ const credentialData: Record = JSON.parse( 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) - .sort(([a], [b]) => a.localeCompare(b, locale)) - .map<[string, string]>(([key, value]) => { - if (typeof value === "string") { - return [key, value]; - } - - return [key, JSON.stringify(value)]; - }); + return [key, JSON.stringify(value)]; + }); }, [credential.credentialData]); return ( diff --git a/src/utils/useLocaleSort.ts b/src/utils/useLocaleSort.ts new file mode 100644 index 0000000000..bc1a915e36 --- /dev/null +++ b/src/utils/useLocaleSort.ts @@ -0,0 +1,38 @@ +import { useWhoAmI } from "../context/whoami/WhoAmI"; + +export type ValueMapperFn = (item: T) => string | undefined; + +export default function useLocaleSort() { + const { whoAmI } = useWhoAmI(); + + return function localeSort(items: T[], mapperFn: ValueMapperFn): 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 = { + [K in keyof T]: T[K] extends V ? K : never; +}[keyof T]; + +export const mapByKey = + < + T extends { [_ in K]?: string }, + K extends KeysMatching + >( + key: K + ) => + (item: T) => + item[key];