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");
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", () => {

View file

@ -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<AuthenticationProviderRepresentation[]>();
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 (
<Modal
@ -121,7 +130,7 @@ export const AddStepModal = ({ name, type, onSelect }: AddStepModalProps) => {
>
{providers && providers.length > max && (
<PaginatingTableToolbar
count={page?.length || 0}
count={page.length || 0}
first={first}
max={max}
onNextClick={setFirst}

View file

@ -20,6 +20,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { useRealm } from "../context/realm-context/RealmContext";
import { emptyFormatter } from "../util";
import useLocaleSort, { mapByKey } from "../utils/useLocaleSort";
import {
CellDropdown,
ClientScope,
@ -32,9 +33,7 @@ import {
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,
@ -48,9 +47,10 @@ import type { Row } from "../clients/scopes/ClientScopes";
import { getProtocolName } from "../clients/utils";
import helpUrls from "../help-urls";
import "./client-scope.css";
export default function ClientScopesSection() {
const { realm } = useRealm();
const { whoAmI } = useWhoAmI();
const { t } = useTranslation("client-scopes");
const adminClient = useAdminClient();
@ -66,6 +66,7 @@ export default function ClientScopesSection() {
AllClientScopes.none
);
const [searchProtocol, setSearchProtocol] = useState<ProtocolType>("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({

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 { 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<ProtocolMapperRepresentation[]>([]);
const [selectedRows, setSelectedRows] = useState<Row[]>([]);
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]
);

View file

@ -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")));
},
[]
);

View file

@ -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<SearchType>("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 [
{

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 { 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<EditClientPolicyConditionParams>();
const adminClient = useAdminClient();
const { whoAmI } = useWhoAmI();
const [open, setOpen] = useState(false);
const localeSort = useLocaleSort();
const [clientScopes, setClientScopes] = useState<ClientScopeRepresentation[]>(
[]
);
@ -62,11 +62,12 @@ export const MultivaluedScopesComponent = ({
<>
{open && (
<AddScopeDialog
clientScopes={clientScopes
.filter((scope) => !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)}

View file

@ -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<ManagementPermissionReference>();
const localeSort = useLocaleSort();
const togglePermissionEnabled = (enabled: boolean) => {
switch (type) {
@ -173,48 +173,47 @@ export const PermissionsTab = ({ id, type }: PermissionsTabProps) => {
</Tr>
</Thead>
<Tbody>
{Object.entries(permission.scopePermissions || {})
.sort((a, b) =>
a[0]!.localeCompare(b[0]!, whoAmI.getLocale())
)
.map(([name, id]) => (
<Tr key={id}>
<Td>
<Link
to={toPermissionDetails({
realm,
id: realmId,
permissionType: "scope",
permissionId: id,
})}
>
{name}
</Link>
</Td>
<Td>
{t(`scopePermissions.${type}.${name}-description`)}
</Td>
<Td isActionCell>
<ActionsColumn
items={[
{
title: t("common:edit"),
onClick() {
history.push(
toPermissionDetails({
realm,
id: realmId,
permissionType: "scope",
permissionId: id,
})
);
},
{localeSort(
Object.entries(permission.scopePermissions || {}),
([name]) => name
).map(([name, id]) => (
<Tr key={id}>
<Td>
<Link
to={toPermissionDetails({
realm,
id: realmId,
permissionType: "scope",
permissionId: id,
})}
>
{name}
</Link>
</Td>
<Td>
{t(`scopePermissions.${type}.${name}-description`)}
</Td>
<Td isActionCell>
<ActionsColumn
items={[
{
title: t("common:edit"),
onClick() {
history.push(
toPermissionDetails({
realm,
id: realmId,
permissionType: "scope",
permissionId: id,
})
);
},
]}
/>
</Td>
</Tr>
))}
},
]}
/>
</Td>
</Tr>
))}
</Tbody>
</TableComposable>
</CardBody>

View file

@ -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 = () => {
</DescriptionListTerm>
<DescriptionListDescription>
<List variant={ListVariant.inline}>
{serverInfo.profileInfo?.disabledFeatures?.map(
(feature) => (
<ListItem key={feature}>{feature}</ListItem>
)
)}
{disabledFeatures.map((feature) => (
<ListItem key={feature}>{feature}</ListItem>
))}
</List>
</DescriptionListDescription>
</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 { 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<ComponentRepresentation[]>([]);
const localeSort = useLocaleSort();
const { id } = useParams<{ id: string }>();
const [selectedMapper, setSelectedMapper] =
useState<ComponentRepresentation>();
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 = () => {
<DeleteConfirm />
<KeycloakDataTable
key={key}
loader={loader}
loader={mappers}
ariaLabelKey="ldapMappersList"
searchPlaceholderKey="common:searchForMapper"
toolbarItem={

View file

@ -10,8 +10,8 @@ import {
} from "@patternfly/react-core";
import type CredentialRepresentation from "@keycloak/keycloak-admin-client/lib/defs/credentialRepresentation";
import { useWhoAmI } from "../../context/whoami/WhoAmI";
import useToggle from "../../utils/useToggle";
import useLocaleSort from "../../utils/useLocaleSort";
import { CredentialDataDialog } from "./CredentialDataDialog";
type CredentialRowProps = {
@ -30,8 +30,7 @@ export const CredentialRow = ({
const { t } = useTranslation("users");
const [showData, toggleShow] = useToggle();
const [kebabOpen, toggleKebab] = useToggle();
const { whoAmI } = useWhoAmI();
const localeSort = useLocaleSort();
const rows = useMemo(() => {
if (!credential.credentialData) {
@ -41,17 +40,15 @@ export const CredentialRow = ({
const credentialData: Record<string, unknown> = 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 (

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];